diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cd38f4..203dad6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ## Changelog +## v1.3.0 + +- ✨ feat: 支持传入变量如当前文档id [#14](https://github.com/frostime/sy-bookmark-plus/issues/14) + - 相关用法已更新 README +- ✨ feat: 支持从文档树中直接拖入文档加入书签中 [#16](https://github.com/frostime/sy-bookmark-plus/issues/16) + ## v1.2.3 - ⚡ perf: 适配优化移动端 [#13](https://github.com/frostime/sy-bookmark-plus/issues/13) diff --git a/README.md b/README.md index 45d3bae..e2eab59 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,38 @@ Dynamic bookmark groups mainly acquire bookmark items by executing queries. ![](./asset/dynamic-group.gif) +### Variable Rendering + +In dynamic groups, variable rendering is supported based on `{{VarName}}`. Variable rendering allows you to insert dynamic variables into rules, which will be replaced with actual values during rendering. Currently supported variables include: + +* `{{CurDocId}}`: ID of the currently active document +* `{{CurRootId}}`: Alias of `{{CurDocId}}` +* `{{yyyy}}`: Current year (four digits) +* `{{MM}}`: Current month (two digits) +* `{{dd}}`: Current day (two digits) +* `{{yy}}`: Last two digits of the current year +* `{{today}}`: Current date (equivalent to `{{yyyy}}{{MM}}{{dd}}`) + +Example 1, SQL rule: View all updates for the current month + +```sql +select * from blocks where +type='d' and updated like '{{yyyy}}{{MM}}%' +order by updated desc +``` + +Example 2, Attribute rule: View all daily notes for the current month + +``` +custom-dailynote-% like {{yyyy}}{{MM}}% +``` + +Example 3, Backlink rule: View backlinks that refer to the current active document: + +``` +{{CurDocId}} +``` + ## Bookmark Items * Click an item to navigate to the corresponding block diff --git a/README_zh_CN.md b/README_zh_CN.md index 8d0d808..1c65ca4 100644 --- a/README_zh_CN.md +++ b/README_zh_CN.md @@ -59,6 +59,38 @@ ![](./asset/dynamic-group.gif) +### 变量渲染 + +在动态组中,支持基于 `{{VarName}}` 的变量渲染。变量渲染允许你在规则中插入一些动态变量,这些变量会在渲染时被替换为实际的值。目前支持的变量包括: + +* `{{CurDocId}}`:当前活动文档的 ID +* `{{CurRootId}}`:`{{CurDocId}}` 的别名,二者等价 +* `{{yyyy}}`:当前年份(四位数) +* `{{MM}}`:当前月份(两位数) +* `{{dd}}`:当前日期(两位数) +* `{{yy}}`:当前年份的后两位数 +* `{{today}}`:当前日期(等价于 `{{yyyy}}{{MM}}{{dd}}`) + + +案例1,SQL 规则:查看本月所有更新 + +```sql +select * from blocks where +type='d' and updated like '{{yyyy}}{{MM}}%' +order by updated desc +``` + +案例2,属性规则:查看本月所有日记 + +``` +custom-dailynote-% like {{yyyy}}{{MM}}% +``` + +案例3,反链规则:查看当前文档的反链: + +``` +{{CurDocId}} +``` ## 书签项目 diff --git a/package.json b/package.json index 31ad87d..90ac76f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sy-bookmark-plus", - "version": "1.2.3", + "version": "1.3.0", "type": "module", "description": "A more powerful bookmark", "repository": "", diff --git a/plugin.json b/plugin.json index 6034ee2..7ac3c68 100644 --- a/plugin.json +++ b/plugin.json @@ -2,7 +2,7 @@ "name": "sy-bookmark-plus", "author": "frostime", "url": "https://github.com/frostime/sy-bookmark-plus", - "version": "1.2.3", + "version": "1.3.0", "minAppVersion": "3.0.12", "backends": [ "all" diff --git a/src/components/group.tsx b/src/components/group.tsx index 7521bb2..0921485 100644 --- a/src/components/group.tsx +++ b/src/components/group.tsx @@ -332,6 +332,10 @@ const Group: Component = (props) => { event.preventDefault(); event.dataTransfer.dropEffect = "copy"; setIsDragOver(true); + } else if (type.startsWith(Constants.SIYUAN_DROP_FILE)) { + event.preventDefault(); + event.dataTransfer.dropEffect = "copy"; + setIsDragOver(true); } else if (type === 'bookmark/item') { event.preventDefault(); event.dataTransfer.dropEffect = "move"; @@ -358,6 +362,21 @@ const Group: Component = (props) => { const info = meta.split(Constants.ZWSP); const nodeId = info[2]; addItemByBlockId(nodeId); + } else if (type.startsWith(Constants.SIYUAN_DROP_FILE)) { + const ele: HTMLElement = window.siyuan.dragElement; + if (ele && ele.innerText) { + const blockid = ele.innerText; + //if '/', it might be the box other than document + if (blockid && blockid !== '/') { + addItemByBlockId(blockid); + } + //Clean up the effect of dragging element + const item: HTMLElement = document.querySelector(`.file-tree.sy__tree li[data-node-id="${blockid}"]`); + if (item) { + item.style.opacity = "1"; + } + window.siyuan.dragElement = undefined; + } } else if (type === 'bookmark/item') { model.moveItem(itemMoving()); } diff --git a/src/components/new-group.tsx b/src/components/new-group.tsx index 59c41b8..ee1665e 100644 --- a/src/components/new-group.tsx +++ b/src/components/new-group.tsx @@ -1,7 +1,8 @@ import { Accessor, createMemo, createSignal, Setter, Show } from "solid-js"; -import ItemWrap from "@/libs/components/item-wrap"; -import InputItem from "@/libs/components/item-input"; +// import ItemWrap from "@/libs/components/item-wrap"; +// import InputItem from "@/libs/components/item-input"; +import Form from "@/libs/components/Form"; import Icon from "./icon"; import { i18n } from "@/utils/i18n"; @@ -33,7 +34,7 @@ const RuleInput = () => { const render = () => { if (ruleType() === 'attr') { return ( - { ); } else if (ruleType() === 'sql') { return ( - { } return ( <> - { />
{i18nPost.name}
- { return (
- - { setRuleInput(''); }} /> - - + +
+ {i18n_.choosetemplate} + { + let temp = template()[key].trim(); + setRuleInput(temp); + setRule({ input: temp }); + }} + style={{ 'width': '130px' }} + /> +
+ + } > - -
- {i18n_.choosetemplate} - { - let temp = template()[key].trim(); - setRuleInput(temp); - setRule({ input: temp }); - }} - style={{ 'width': '130px' }} - /> -
-
-
+
); } @@ -250,7 +250,7 @@ const NewGroup = (props: IPrpos) => { const transitionDuration = 100; return ( -
{ if (e.key === 'Enter') { e.stopImmediatePropagation(); // 防止 enter 让 dialog 直接 confirm 了 @@ -258,11 +258,11 @@ const NewGroup = (props: IPrpos) => { }} >
- - { props.setGroup({ name: v }); }} /> - - + - { } }} /> - +
{ diff --git a/src/components/setting/index.tsx b/src/components/setting/index.tsx index 0b77a99..6720c1d 100644 --- a/src/components/setting/index.tsx +++ b/src/components/setting/index.tsx @@ -1,5 +1,4 @@ -import SettingItemWrap from "@/libs/components/item-wrap"; -import InputItem from "@/libs/components/item-input"; +import { FormWrap as SettingItemWrap, FormInput as InputItem } from '@/libs/components/Form'; import GroupList from './group-list'; import { configs, setConfigs } from "@/model"; @@ -9,7 +8,7 @@ const App = () => { const i18n_ = i18n.setting; return ( -
diff --git a/src/libs/components/item-input.tsx b/src/libs/components/Form/form-input.tsx similarity index 94% rename from src/libs/components/item-input.tsx rename to src/libs/components/Form/form-input.tsx index c19c52f..e67e7cb 100644 --- a/src/libs/components/item-input.tsx +++ b/src/libs/components/Form/form-input.tsx @@ -1,11 +1,11 @@ -import { createMemo, For } from "solid-js"; +import { createMemo, For, JSX } from "solid-js"; interface IProps extends ISettingItemCore { - changed: (v?: any) => void; - style?: { [key: string]: string | number }; + changed?: (v?: any) => void; + style?: JSX.CSSProperties; } -export default function InputItem(props: IProps) { +export default function FormInput(props: IProps) { const fn_size = true; @@ -23,7 +23,7 @@ export default function InputItem(props: IProps) { styles = { resize: "vertical", height: '10rem', "white-space": "nowrap" }; } let propstyle = props.style ?? {}; - styles = { ...styles, ...propstyle }; + styles = {...styles, ...propstyle}; return { style: styles }; @@ -92,7 +92,7 @@ export default function InputItem(props: IProps) { {...attrStyle()} onClick={click} > - {props.button.label} + {props.button?.label ?? props.value} ); } else if (props.type === "select") { diff --git a/src/libs/components/Form/form-wrap.module.css b/src/libs/components/Form/form-wrap.module.css new file mode 100644 index 0000000..02d3929 --- /dev/null +++ b/src/libs/components/Form/form-wrap.module.css @@ -0,0 +1,16 @@ +/* item-wrap.module.css */ + +.item-wrap { + box-shadow: unset; + padding-bottom: 16px; + /* margin-bottom: 16px; */ +} + +.item-wrap:not(:last-child) { + border-bottom: 1px solid var(--b3-border-color); +} + +.title { + font-weight: bold; + color: var(--b3-theme-primary); +} diff --git a/src/libs/components/Form/form-wrap.tsx b/src/libs/components/Form/form-wrap.tsx new file mode 100644 index 0000000..cec1bfc --- /dev/null +++ b/src/libs/components/Form/form-wrap.tsx @@ -0,0 +1,71 @@ +// Copyright (c) 2024 by frostime. All Rights Reserved. +// Author : frostime +// Date : 2024-06-01 20:03:50 +// FilePath : /src/libs/setting-item-wrap.tsx +// LastEditTime : 2024-06-07 19:14:28 +// Description : The setting item container + +import { children, Component, createMemo, JSX } from "solid-js"; + +import css from './form-wrap.module.css'; + +interface IFormWrap { + title: string; + description: string; + direction?: 'row' | 'column'; + children?: JSX.Element; + style?: Record; + action?: JSX.Element; +} + +const FormWrap: Component = (props) => { + + const C = children(() => props.children); + + const A = createMemo(() => props.action); + + const attrStyle = createMemo(() => { + let styles = {}; + if (props.direction === 'column') { + styles = { position: 'relative' }; + } + let propstyle = props.style ?? {}; + styles = { ...styles, ...propstyle }; + return { + style: styles + }; + }); + + return ( + <> + {props.direction === "row" ? ( +
+
+
+
+ {props.title} +
+
+
{A()}
+
+
+
+ {C()} +
+
+
+ ) : ( +
+
+ {props.title} +
+
+ + {C()} +
+ )} + + ); +}; + +export default FormWrap; diff --git a/src/libs/components/Form/index.ts b/src/libs/components/Form/index.ts new file mode 100644 index 0000000..f98da70 --- /dev/null +++ b/src/libs/components/Form/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2024 by frostime. All Rights Reserved. + * @Author : frostime + * @Date : 2024-08-10 21:20:36 + * @FilePath : /src/libs/components/Form/index.ts + * @LastEditTime : 2024-08-10 21:21:09 + * @Description : + */ +import FormWrap from "./form-wrap"; +import FormInput from "./form-input"; + +const Form = { + Wrap: FormWrap, + Input: FormInput +}; +export default Form; +export { FormWrap, FormInput }; diff --git a/src/libs/components/dialog-action.tsx b/src/libs/components/dialog-action.tsx new file mode 100644 index 0000000..afde4c5 --- /dev/null +++ b/src/libs/components/dialog-action.tsx @@ -0,0 +1,27 @@ +import { Component } from "solid-js"; + +interface IDialogActionsProps { + onConfirm: () => void; + onCancel: () => void; +} + +const DialogAction: Component = (props) => { + + return ( +
+ +
+ +
+ ); +}; + +export default DialogAction; diff --git a/src/libs/components/item-wrap.tsx b/src/libs/components/item-wrap.tsx deleted file mode 100644 index e1bbcb0..0000000 --- a/src/libs/components/item-wrap.tsx +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) 2024 by frostime. All Rights Reserved. -// Author : frostime -// Date : 2024-06-01 20:03:50 -// FilePath : /src/libs/setting-item-wrap.tsx -// LastEditTime : 2024-06-07 19:14:28 -// Description : The setting item container - -import { children, Component, JSX } from "solid-js"; - -interface SettingItemWrapProps { - title: string; - description: string; - direction?: 'row' | 'column'; - children?: JSX.Element; -} - -const SettingItemWrap: Component = (props) => { - - const c = children(() => props.children); - - return ( - <> - {props.direction === "row" ? ( -
-
- {props.title} -
-
-
- {c()} -
-
-
- ) : ( -
-
- {props.title} -
-
- - {c()} -
- )} - - ); -}; - -export default SettingItemWrap; diff --git a/src/libs/components/setting-panel.tsx b/src/libs/components/setting-panel.tsx index 4dd0b5b..c16aff7 100644 --- a/src/libs/components/setting-panel.tsx +++ b/src/libs/components/setting-panel.tsx @@ -6,8 +6,7 @@ // Description : import { Component, For, JSXElement, children } from "solid-js"; -import ItemWrap from "./item-wrap"; -import InputItem from "./item-input"; +import Form from "./Form"; interface SettingPanelProps { group: string; @@ -23,12 +22,12 @@ const SettingPanel: Component = (props) => {
{(item) => ( - - = (props) => { button={item?.button} changed={(v) => props.onChanged({ group: props.group, key: item.key, value: v })} /> - + )} {useChildren()} diff --git a/src/libs/template.ts b/src/libs/template.ts deleted file mode 100644 index 19aca5c..0000000 --- a/src/libs/template.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { getActiveDoc } from "@/utils"; - -type TVarName = string; -type TVars = Record string | null>; - - -const DefaultVars: TVars = { - 'yyyy': () => new Date().getFullYear().toString(), - 'mm': () => (new Date().getMonth() + 1).toString().padStart(2, '0'), - 'dd': () => new Date().getDate().toString().padStart(2, '0'), - 'docid': () => getActiveDoc(), -} - -/** - * 将所有模板变量替换为实际的值 - * 模板变量使用 {{\s*(...)\s*}} 包裹 - * 默认模板变量: - * - yyyy: 当前年份 - * - mm: 当前月份 - * - dd: 当前日期 - * - docid: 当前激活中的文档的 id - * @param template 模板字符串 - * @param vars 自定义模板变量 - * @returns (string | null) 替换后的字符串; 如果有模板变量没有被正确替换, 则返回 null - */ -const renderTemplate = (template: string, vars?: TVars): string => { - vars = vars ?? {}; - vars = { ...DefaultVars, ...vars }; - let result = template; - for (const [varName, evaluate] of Object.entries(vars)) { - const varReg = new RegExp(`{{\\s*${varName}\\s*}}`, 'g'); - let value = evaluate(); - if (value === null || value === undefined) return null; - result = result.replace(varReg, value); - } - return result; -} - -export { - renderTemplate -}; diff --git a/src/model/rules.ts b/src/model/rules.ts index 5ddfe25..2c1a049 100644 --- a/src/model/rules.ts +++ b/src/model/rules.ts @@ -3,13 +3,14 @@ * @Author : Yp Z * @Date : 2023-07-29 15:17:15 * @FilePath : /src/model/rules.ts - * @LastEditTime : 2024-07-25 21:06:16 + * @LastEditTime : 2024-08-26 13:18:55 * @Description : */ import * as api from "@/api"; import { fb2p, getBlocksByIDs } from "@/libs/query"; import { Caret } from "@/utils/const"; +import { renderTemplate, VAR_NAMES } from "./templating"; export abstract class MatchRule implements IDynamicRule { @@ -82,19 +83,22 @@ export class Backlinks extends MatchRule { } validateInput(): boolean { - return matchIDFormat(this.id) && ['', 'fb2p', 'b2doc'].includes(this.process); + const validateID = matchIDFormat(this.id) || this.id === `{{${VAR_NAMES.CurDocId}}}` || this.id === `{{${VAR_NAMES.CurRootId}}}`; + const validProcess = ['', 'fb2p', 'b2doc'].includes(this.process); + return validateID && validProcess; } async fetch() { this.eof = true; - if (!this.id) { + let runtimeId = renderTemplate(this.id); + if (!runtimeId) { return []; } const sql = ` select blocks.* from blocks join refs on blocks.id = refs.block_id - where refs.def_block_id = '${this.id}' + where refs.def_block_id = '${runtimeId}' order by blocks.updated desc limit 999; `; @@ -142,10 +146,11 @@ export class SQL extends MatchRule { async fetch() { this.eof = true; - if (!this.input) { + let sqlCode = renderTemplate(this.input); + if (!sqlCode) { return []; } - let result = await api.sql(this.input); + let result = await api.sql(sqlCode); return result ?? []; } } @@ -168,7 +173,7 @@ class Attr extends MatchRule { * @returns */ validateInput(): boolean { - const inputPattern = /^([\-\w\%]+)(?:\s*(=|like)\s*(.+))?$/; + const inputPattern = /^([\-\w\%\{\}]+)(?:\s*(=|like)\s*(.+))?$/; let ok = inputPattern.test(this._input); if (!ok) return false; const matches = this._input.match(inputPattern); @@ -186,14 +191,17 @@ class Attr extends MatchRule { } async fetch() { + let name = renderTemplate(this.attrname); + let value = this.attrval ? renderTemplate(this.attrval) : ''; + // if (!name || !value) return []; let query = ` SELECT B.* FROM blocks AS B WHERE B.id IN ( SELECT A.block_id FROM attributes AS A - WHERE A.name like '${this.attrname}' - ${this.attrval ? `AND A.value ${this.attrop} '${this.attrval}'` : ''} + WHERE A.name like '${name}' + ${value ? `AND A.value ${this.attrop} '${value}'` : ''} );`; let result = await api.sql(query); return result ?? []; diff --git a/src/model/templating.ts b/src/model/templating.ts new file mode 100644 index 0000000..2862e4a --- /dev/null +++ b/src/model/templating.ts @@ -0,0 +1,64 @@ +import { getActiveDoc } from "@/utils"; + +/* + * Copyright (c) 2024 by frostime. All Rights Reserved. + * @Author : frostime + * @Date : 2024-08-26 11:32:09 + * @FilePath : /src/model/templating.ts + * @LastEditTime : 2024-08-26 12:14:45 + * @Description : 允许规则中插入一些变量;参考 https://github.com/frostime/sy-bookmark-plus/issues/14 + */ +export const VAR_NAMES = { + CurDocId: 'CurDocId', + CurRootId: 'CurRootId', + Year: 'yyyy', + Month: 'MM', + Day: 'dd', + ShortYear: 'yy', + Today: 'today' +} + +const AllowedVars = Object.values(VAR_NAMES); + +/** + * 判断输入的字符串是否是合法的变量,格式为 {{varName}}; varName 为 VAR_NAMES 中的值 + * @param str + * @returns + */ +export const isVar = (str: string) => { + return str.startsWith('{{') && str.endsWith('}}') && AllowedVars.includes(str.slice(2, -2)); +} + + +const renderString = (template: string, data: { [key: string]: string }) => { + //{{var name}} + for (const varName of AllowedVars) { + const regex = new RegExp(`{{${varName}}}`, 'g'); + template = template.replace(regex, data[varName]); + } + return template; +} + +/** + * 动态渲染模板字符串 + * @param text + * @returns + */ +export const renderTemplate = (text: string) => { + let docId = getActiveDoc(); + let now = new Date(); + let year = now.getFullYear(); + let month = now.getMonth() + 1; + let day = now.getDate(); + let data = { + 'CurDocId': docId, + 'CurRootId': docId, + + 'yyyy': year.toString(), + 'MM': month.toString().padStart(2, '0'), + 'dd': day.toString().padStart(2, '0'), + 'yy': year.toString().slice(-2), + 'today': `${year}${month.toString().padStart(2, '0')}${day.toString().padStart(2, '0')}` + }; + return renderString(text, data); +} diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 3041385..5e7a5b2 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -118,6 +118,7 @@ declare interface Window { ws: any; languages: any; emojis: any; + dragElement?: HTMLDivElement; }; Lute: { Caret: string;