├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── .npmrc
├── LICENSE
├── README.md
├── esbuild.config.mjs
├── main.ts
├── manifest.json
├── package.json
├── src
├── assets
│ └── demo.gif
├── components
│ ├── command-menu.ts
│ ├── link-input-modal.ts
│ ├── plugin-setting.ts
│ └── selection-menu.ts
├── constants.ts
└── util
│ ├── cmd-generate.ts
│ ├── link-bookmark.ts
│ └── util.ts
├── styles.css
├── tsconfig.json
├── version-bump.mjs
└── versions.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # top-most EditorConfig file
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | insert_final_newline = true
8 | indent_style = tab
9 | indent_size = 4
10 | tab_width = 4
11 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
3 | main.js
4 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "env": { "node": true },
5 | "plugins": [
6 | "@typescript-eslint"
7 | ],
8 | "extends": [
9 | "eslint:recommended",
10 | "plugin:@typescript-eslint/eslint-recommended",
11 | "plugin:@typescript-eslint/recommended"
12 | ],
13 | "parserOptions": {
14 | "sourceType": "module"
15 | },
16 | "rules": {
17 | "no-unused-vars": "off",
18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
19 | "@typescript-eslint/ban-ts-comment": "off",
20 | "no-prototype-builtins": "off",
21 | "@typescript-eslint/no-empty-function": "off"
22 | }
23 | }
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release Obsidian plugin
2 |
3 | on:
4 | push:
5 | tags:
6 | - "*"
7 |
8 | env:
9 | PLUGIN_NAME: typing-assistant
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 | - name: Use Node.js
18 | uses: actions/setup-node@v1
19 | with:
20 | node-version: "14.x"
21 |
22 | - name: Build
23 | id: build
24 | run: |
25 | yarn install
26 | yarn build
27 | mkdir ${{ env.PLUGIN_NAME }}
28 | cp main.js manifest.json styles.css ${{ env.PLUGIN_NAME }}
29 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }}
30 | ls
31 | echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)"
32 |
33 | - name: Create Release
34 | id: create_release
35 | uses: actions/create-release@v1
36 | env:
37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
38 | VERSION: ${{ github.ref }}
39 | with:
40 | tag_name: ${{ github.ref }}
41 | release_name: ${{ github.ref }}
42 | draft: false
43 | prerelease: false
44 |
45 | - name: Upload zip file
46 | id: upload-zip
47 | uses: actions/upload-release-asset@v1
48 | env:
49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
50 | with:
51 | upload_url: ${{ steps.create_release.outputs.upload_url }}
52 | asset_path: ./${{ env.PLUGIN_NAME }}.zip
53 | asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip
54 | asset_content_type: application/zip
55 |
56 | - name: Upload main.js
57 | id: upload-main
58 | uses: actions/upload-release-asset@v1
59 | env:
60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
61 | with:
62 | upload_url: ${{ steps.create_release.outputs.upload_url }}
63 | asset_path: ./main.js
64 | asset_name: main.js
65 | asset_content_type: text/javascript
66 |
67 | - name: Upload manifest.json
68 | id: upload-manifest
69 | uses: actions/upload-release-asset@v1
70 | env:
71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
72 | with:
73 | upload_url: ${{ steps.create_release.outputs.upload_url }}
74 | asset_path: ./manifest.json
75 | asset_name: manifest.json
76 | asset_content_type: application/json
77 |
78 | - name: Upload styles.css
79 | id: upload-css
80 | uses: actions/upload-release-asset@v1
81 | env:
82 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
83 | with:
84 | upload_url: ${{ steps.create_release.outputs.upload_url }}
85 | asset_path: ./styles.css
86 | asset_name: styles.css
87 | asset_content_type: text/css
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # vscode
2 | .vscode
3 |
4 | # Intellij
5 | *.iml
6 | .idea
7 |
8 | # npm
9 | node_modules
10 |
11 | # Don't include the compiled main.js file in the repo.
12 | # They should be uploaded to GitHub releases instead.
13 | main.js
14 |
15 | # Exclude sourcemaps
16 | *.map
17 |
18 | # obsidian
19 | data.json
20 |
21 | # Exclude macOS Finder (System Explorer) View States
22 | .DS_Store
23 |
24 | yarn.lock
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | tag-version-prefix=""
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Jambo
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 | # Typing Assistant
2 | Typing Assistant is a plugin that improves writing efficiency and provides a user experience similar to that of Notion
3 |
4 | 
5 |
6 | ## Demo 1
7 | 
8 |
9 | ## Demo 2
10 | https://github.com/alisahanyalcin/Typing-Assistant/assets/34830846/0994312e-ab97-45b1-b810-d1a9b62b9c40
11 |
12 | ## Usage
13 | - Install and activate the plugin
14 | - When you input a '/' in an empty line or the end of a line that not empty, you will get a command menu to help you choose type of the new line
15 | - At any time, the selected text will evoke the selection-menu for quick setting to switch the style of the selected text,and also supports row style switching
16 |
17 | ## Features
18 | - Support to create multiple types of line text by invoke the shortcut menu
19 | - Support settings regular styles of markdown to selected text
20 | - The menu actively follows the cursor position of writing
21 | - Support input link address to generate personalized card
22 | - Supports custom command combinations and drag-and-drop sorting
23 | - Support quick search of commands
24 |
25 | ## Installation
26 | - Open Settings > Third-Party Add-ons
27 | - Make sure safe mode is off
28 | - Click to browse community plugins
29 | - Search for "Typing Assistant"
30 | - click install
31 | - Once installed, close the community plugin window and activate the newly installed plugin
32 |
--------------------------------------------------------------------------------
/esbuild.config.mjs:
--------------------------------------------------------------------------------
1 | import esbuild from "esbuild";
2 | import process from "process";
3 | import builtins from "builtin-modules";
4 |
5 | const banner =
6 | `/*
7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
8 | if you want to view the source, please visit the github repository of this plugin
9 | */
10 | `;
11 |
12 | const prod = (process.argv[2] === "production");
13 |
14 | const context = await esbuild.context({
15 | banner: {
16 | js: banner,
17 | },
18 | entryPoints: ["main.ts"],
19 | bundle: true,
20 | external: [
21 | "obsidian",
22 | "electron",
23 | "@codemirror/autocomplete",
24 | "@codemirror/collab",
25 | "@codemirror/commands",
26 | "@codemirror/language",
27 | "@codemirror/lint",
28 | "@codemirror/search",
29 | "@codemirror/state",
30 | "@codemirror/view",
31 | "@lezer/common",
32 | "@lezer/highlight",
33 | "@lezer/lr",
34 | ...builtins],
35 | format: "cjs",
36 | target: "es2018",
37 | logLevel: "info",
38 | sourcemap: prod ? false : "inline",
39 | treeShaking: true,
40 | outfile: "main.js",
41 | });
42 |
43 | if (prod) {
44 | await context.rebuild();
45 | process.exit(0);
46 | } else {
47 | await context.watch();
48 | }
--------------------------------------------------------------------------------
/main.ts:
--------------------------------------------------------------------------------
1 | import { MarkdownView, Plugin, Platform } from "obsidian";
2 | import { CommandMenu } from "src/components/command-menu";
3 | import {
4 | CMD_CONFIG,
5 | CODE_LAN,
6 | CONTENT_MAP,
7 | HEADING_MENU,
8 | ICON_MAP,
9 | TEXT_MAP,
10 | } from "src/constants";
11 | import { InsertLinkModal } from "src/components/link-input-modal";
12 | import { SelectionBtns } from "src/components/selection-menu";
13 | import { type ExamplePluginSettings, ExampleSettingTab, CMD_TYPE } from "src/components/plugin-setting";
14 | import { linkParse, loadIcons, loadCommands, generateBookMark, isLineEdit, isLineSelect } from "src/util/util";
15 |
16 | const { isMobile } = Platform;
17 | export default class TypingAsstPlugin extends Plugin {
18 | commands: CommandMenu;
19 | btns: SelectionBtns;
20 | linkModal: InsertLinkModal;
21 | scrollArea?: Element;
22 | settings: ExamplePluginSettings;
23 |
24 | async loadSettings() {
25 | const initialMenu = HEADING_MENU.filter(item => item === 'insert-note-callout' || !item.includes("callout"));
26 | this.settings = Object.assign({}, { showPlaceholder: true, cmdsSorting: initialMenu, disableSelectionMenu: isMobile }, await this.loadData());
27 | // console.log('commands======>', this.app.commands.commands)
28 | }
29 |
30 | async saveSettings() {
31 | await this.saveData(this.settings);
32 | }
33 |
34 | async onload() {
35 |
36 | await this.loadSettings();
37 |
38 | loadCommands.call(this)
39 |
40 | this.addSettingTab(new ExampleSettingTab(this.app, this));
41 |
42 | // import svg to Obsidian-Icon
43 | loadIcons(ICON_MAP);
44 |
45 | const onSelectionAction = async (
46 | content: string,
47 | isHeading: boolean
48 | ) => {
49 | const view =
50 | this.app.workspace.getActiveViewOfType(MarkdownView);
51 | if (!view?.editor) return;
52 | if (isHeading) {
53 | const editor = view.editor;
54 | const cursor = editor.getCursor();
55 | const lineContent = editor.getLine(cursor.line);
56 | editor.setLine(cursor.line, lineContent.replace(/^.*?\s/, ""));
57 | }
58 | (this.app as any).commands.executeCommandById((CMD_CONFIG as any)[content].cmd);
59 | if (content === "set-link") {
60 | view.editor.focus();
61 | }
62 | this.btns.hide();
63 | };
64 |
65 |
66 | const onMenuClick = async (content: CMD_TYPE) => {
67 | const view = this.app.workspace.getActiveViewOfType(MarkdownView);
68 | if (view) {
69 | (this.app as any).commands.executeCommandById(
70 | CMD_CONFIG[content].cmd
71 | );
72 | view.editor.focus();
73 |
74 | if (content === CONTENT_MAP['bookmark']) {
75 | view.editor.blur();
76 | this.linkModal.open();
77 | }
78 | }
79 | };
80 |
81 | const onLinkSubmit = async (url: string) => {
82 | const parsedResult = await linkParse(url);
83 | let codeStr = "```" + CODE_LAN + "\n";
84 | for (const key in parsedResult) {
85 | codeStr += key + ":" + (parsedResult as any)[key] + "\n";
86 | }
87 | codeStr += "```\n";
88 | const view = this.app.workspace.getActiveViewOfType(MarkdownView);
89 | if (view) {
90 | const cursor = view.editor.getCursor();
91 | const editLine = view.editor.getLine(cursor.line);
92 | if (editLine.length > 0) {
93 | view.editor.replaceRange(
94 | `\n${codeStr}`,
95 | { line: cursor.line, ch: cursor.ch - 1 },
96 | cursor
97 | );
98 | view.editor.setCursor({
99 | line: cursor.line + 1,
100 | ch: codeStr.length,
101 | });
102 | } else {
103 | view.editor.setLine(cursor.line, codeStr);
104 | view.editor.setCursor({
105 | line: cursor.line,
106 | ch: codeStr.length,
107 | });
108 | }
109 | }
110 | };
111 |
112 | const handleSelection = () => {
113 | const selection = document.getSelection()?.toString();
114 | if (selection) {
115 | const view =
116 | this.app.workspace.getActiveViewOfType(MarkdownView);
117 | if (!view?.editor) return;
118 | const editor = view.editor;
119 | const cursor = editor.getCursor();
120 | const lineContent = editor.getLine(cursor.line);
121 |
122 | let lineStyle = "Text";
123 | for (const cmd in CONTENT_MAP) {
124 | if (cmd === "text") {
125 | continue;
126 | // } else if (/^\> \[!/.test(lineContent)) {
127 | // lineStyle = TEXT_MAP["callout"];
128 | // break;
129 | } else if (/^\`\`\`/.test(lineContent)) {
130 | lineStyle = TEXT_MAP["code"];
131 | break;
132 | } else if (
133 | lineContent.startsWith((CONTENT_MAP as any)[cmd])
134 | ) {
135 | lineStyle = (TEXT_MAP as any)[cmd];
136 | break;
137 | } else if (/^[\d]+\.\s/.test(lineContent)) {
138 | lineStyle = TEXT_MAP["numberList"];
139 | break;
140 | }
141 | }
142 | this.btns?.display(lineStyle);
143 | }
144 | };
145 |
146 | this.linkModal = new InsertLinkModal(this.app, onLinkSubmit);
147 |
148 | this.registerDomEvent(document, "click", (evt: MouseEvent) => {
149 | this.commands?.hide();
150 | const selection = document.getSelection()?.toString();
151 | if (!selection && this.btns?.isVisible()) {
152 | this.btns.hide();
153 | }
154 | });
155 |
156 | this.registerDomEvent(document, "mouseup", (evt: MouseEvent) => {
157 | // prevent title or code selection
158 | if (!isLineSelect(evt?.target)) return;
159 | // desable seletion menu in mobile env
160 | // if (isMobile) return;
161 | if(this.settings.disableSelectionMenu)return;
162 | handleSelection();
163 | });
164 |
165 | this.registerDomEvent(document, "keydown", (evt: KeyboardEvent) => {
166 | if ((evt?.target as any)?.getAttribute?.('class')?.includes('command-option')) {
167 | const view =
168 | this.app.workspace.getActiveViewOfType(MarkdownView);
169 | if (view) {
170 | view.editor.focus();
171 | }
172 | }
173 | })
174 |
175 | this.registerDomEvent(document, "keyup", (evt: KeyboardEvent) => {
176 | if (this.commands?.isVisible()) {
177 | const { key } = evt;
178 | if (
179 | key !== "ArrowUp" &&
180 | key !== "ArrowDown" &&
181 | key !== "ArrowLeft" &&
182 | key !== "ArrowRight"
183 | ) {
184 | const view =
185 | this.app.workspace.getActiveViewOfType(MarkdownView);
186 | if (view) {
187 | const cursor = view.editor.getCursor();
188 | const editLine = view.editor.getLine(cursor.line);
189 | const _cmd = (editLine?.match(/[^\/]*$/)?.[0] || '')
190 | if (!editLine) {
191 | this.commands?.hide();
192 | } else {
193 | this.commands?.search(_cmd);
194 | }
195 | view.editor.focus();
196 | }
197 | }
198 | }
199 | this.btns?.hide();
200 | });
201 | // const scrollEvent = () => {
202 | // if (this.btns?.isVisible()) {
203 | // // handleSelection();
204 | // }
205 | // };
206 |
207 | const renderPlugin = () => {
208 | const showEmptyPrompt = this.settings.showPlaceholder;
209 | document.documentElement.style.setProperty('--show-empty-prompt', showEmptyPrompt ? 'block' : 'none')
210 |
211 | const view = this.app.workspace.getActiveViewOfType(MarkdownView);
212 | if (!view) return;
213 | this.scrollArea =
214 | view.containerEl.querySelector(".cm-scroller") ?? undefined;
215 | const appHeader = document.querySelector(".titlebar");
216 | const viewHeader = view.containerEl.querySelector(".view-header");
217 | const headerHeight =
218 | (appHeader?.clientHeight ?? 0) +
219 | (viewHeader?.clientHeight ?? 0);
220 |
221 | if (!this.scrollArea) return;
222 | const scrollArea = this.scrollArea;
223 |
224 | this.commands?.remove();
225 | this.commands = new CommandMenu({
226 | scrollArea,
227 | onMenu: onMenuClick,
228 | defaultCmds: this.settings.cmdsSorting
229 | });
230 |
231 | this.btns?.remove();
232 | this.btns = new SelectionBtns({
233 | scrollArea,
234 | headerHeight,
235 | onAction: onSelectionAction,
236 | });
237 |
238 | // scrollArea?.addEventListener("scroll", scrollEvent);
239 | };
240 |
241 | /**Ensure that the plugin can be loaded and used immediately after it is turned on */
242 | renderPlugin();
243 |
244 | this.registerEvent(
245 | this.app.workspace.on("active-leaf-change", renderPlugin)
246 | );
247 |
248 | this.registerDomEvent(document, "input", (evt: InputEvent) => {
249 | if (!isLineEdit(evt?.target)) return;
250 | if (this.linkModal.isOpen) return;
251 | if (evt && evt.data === "/") {
252 | const view =
253 | this.app.workspace.getActiveViewOfType(MarkdownView);
254 | if (!view) return;
255 | const cursor = view.editor.getCursor();
256 | const editLine = view.editor.getLine(cursor.line);
257 | if (editLine.replace(/[\s]*$/, "").length <= cursor.ch) {
258 | this.commands?.display();
259 | } else {
260 | // this.commands?.hide();
261 | }
262 | }
263 | });
264 |
265 | this.registerMarkdownCodeBlockProcessor(CODE_LAN, (source, el, ctx) => {
266 | const bookmark = generateBookMark(source);
267 | el?.appendChild(bookmark);
268 | });
269 | }
270 |
271 | onunload() {
272 | this.commands?.remove();
273 | this.btns?.remove();
274 | }
275 | }
276 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "typing-assistant",
3 | "name": "Typing Assistant",
4 | "version": "0.2.8",
5 | "minAppVersion": "0.15.0",
6 | "description": "Support multiple shortcut menus to improve input efficiency",
7 | "author": "Jambo",
8 | "authorUrl": "https://github.com/Jambo2018",
9 | "isDesktopOnly": false
10 | }
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "typing-assistant",
3 | "version": "0.2.6",
4 | "description": "Notion Assistant is a plugin that improves input efficiency and provides a user experience similar to that of【Notion】",
5 | "main": "main.js",
6 | "scripts": {
7 | "dev": "node esbuild.config.mjs",
8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
9 | "version": "node version-bump.mjs && git add manifest.json versions.json"
10 | },
11 | "keywords": [],
12 | "author": "Jambo",
13 | "repository": "https://github.com/Jambo2018/notion-assistant-plugin",
14 | "license": "MIT",
15 | "devDependencies": {
16 | "@types/node": "^16.11.6",
17 | "@typescript-eslint/eslint-plugin": "5.29.0",
18 | "@typescript-eslint/parser": "5.29.0",
19 | "builtin-modules": "3.3.0",
20 | "esbuild": "0.17.3",
21 | "obsidian": "latest",
22 | "tslib": "2.4.0",
23 | "typescript": "4.7.4"
24 | },
25 | "dependencies": {
26 | "@types/sortablejs": "^1.15.7",
27 | "fuzzy": "^0.1.3",
28 | "sortablejs": "^1.15.0"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/assets/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Jambo2018/notion-assistant-plugin/348a99e63b54ce8db17edbb04214cd307e9c0403/src/assets/demo.gif
--------------------------------------------------------------------------------
/src/components/command-menu.ts:
--------------------------------------------------------------------------------
1 | import { setIcon } from "obsidian";
2 | import {
3 | CMD_CONFIG,
4 | CMD_CONFIG_ARR,
5 | COMMAD_ITEM_EIGHT,
6 | HEADING_MENU,
7 | MAX_MENU_HEIGHT,
8 | MENU_MARGIN,
9 | MENU_WIDTH,
10 | } from "../constants";
11 | import { CMD_TYPE } from "./plugin-setting";
12 | import fuzzy from "fuzzy";
13 |
14 | /**
15 | * show the menu while input a '/' in an empty line or the end of a line that not empty
16 | */
17 | export class CommandMenu {
18 | menu: HTMLDivElement;
19 | scrollArea?: HTMLDivElement;
20 | mouseMoved: boolean;
21 | menuHieght: number;
22 | defaultCmds: CMD_TYPE[]
23 | callback: (cmd: string) => void;
24 | constructor(props: {
25 | scrollArea?: Element;
26 | onMenu: (content: string) => void;
27 | defaultCmds: CMD_TYPE[]
28 | }) {
29 | this.menu = createDiv({ cls: "command", attr: { id: "command-menu" } });
30 | this.mouseMoved = false;
31 | this.defaultCmds = props.defaultCmds;
32 | this.scrollArea = props.scrollArea as HTMLDivElement;
33 | this.menuHieght = Math.min(COMMAD_ITEM_EIGHT * props.defaultCmds.length, MAX_MENU_HEIGHT)
34 | this.callback = props.onMenu;
35 | this.scrollArea.appendChild(this.menu);
36 | this.hide();
37 | }
38 |
39 | display = function () {
40 | const range = window?.getSelection()?.getRangeAt(0);
41 | const rect = range?.getBoundingClientRect();
42 | const scroll = this.scrollArea.getBoundingClientRect();
43 | if (!rect) return;
44 | let { height, top, left } = rect;
45 | top += height + this.scrollArea.scrollTop;
46 | top -= scroll.top;
47 | left -= scroll.left;
48 | const rightDis = left + MENU_WIDTH - scroll.width;
49 | if (rightDis > 0) {
50 | left -= rightDis;
51 | }
52 |
53 | const upDis =
54 | top +
55 | this.menuHieght +
56 | MENU_MARGIN -
57 | this.scrollArea.scrollTop -
58 | this.scrollArea.clientHeight;
59 | if (upDis > 0) {
60 | this.scrollArea.scrollTo(0, this.scrollArea.scrollTop + upDis);
61 | }
62 |
63 | this.menu.style = `top:${top}px;left:${left}px`;
64 | if (!this.isVisible()) {
65 | this.menu.removeClass("display-none");
66 | }
67 | this.scrollArea?.addClass("scroll-disable");
68 | this.search('')
69 | };
70 |
71 | search = function (str: string) {
72 | let _cmds = []
73 | if (!str) {
74 | _cmds = this.defaultCmds;
75 | } else {
76 | _cmds = fuzzy.filter(str || '', CMD_CONFIG_ARR, { extract: (e: any) => e.title }).map((e: any) => e.original.cmd).filter((e: any) => HEADING_MENU.includes(e))
77 | }
78 | this.generateMenu(_cmds)
79 | }
80 |
81 | private generateMenu = function (_cmds: CMD_TYPE[]) {
82 | if (_cmds.length === 0) {
83 | this.hide()
84 | }
85 | while (this.menu.firstChild) {
86 | this.menu.firstChild.remove();
87 | }
88 | const _this = this;
89 | _cmds.forEach((item, idx) => {
90 | const btn = createDiv({
91 | parent: this.menu,
92 | cls: "command-option",
93 | attr: { tabindex: -1, commandType: item },
94 | });
95 | const IconDiv = createDiv({ parent: btn });
96 | setIcon(IconDiv, CMD_CONFIG[item].icon);
97 | btn.createSpan({ text: CMD_CONFIG[item].title });
98 | btn.onclick = function () {
99 | _this.callback(item);
100 | };
101 | btn.onmouseenter = function () {
102 | if (_this.mouseMoved) {
103 | btn.focus();
104 | }
105 | _this.mouseMoved = false;
106 | };
107 | });
108 | setTimeout(() => {
109 | this.menu.children?.[0]?.focus?.()
110 | });
111 |
112 | this.menu.onmousemove = function () {
113 | _this.mouseMoved = true;
114 | };
115 | this.menu.onkeydown = function (e: any) {
116 | const { key } = e;
117 | if (
118 | key === "ArrowUp" ||
119 | key === "ArrowDown" ||
120 | key === "ArrowLeft" ||
121 | key === "ArrowRight" ||
122 | key === "Enter"
123 | ) {
124 | const focusEle = document.activeElement;
125 | const cmd = focusEle?.getAttribute("commandType");
126 | if (!cmd) return;
127 | e?.preventDefault();
128 | e?.stopPropagation();
129 | if (key === "Enter") {
130 | _this.callback(cmd);
131 | _this.hide();
132 | }
133 | let nextFocusEle: HTMLElement = focusEle as HTMLElement;
134 | if (key === "ArrowUp" || key === "ArrowLeft") {
135 | nextFocusEle =
136 | focusEle?.previousElementSibling as HTMLElement;
137 | if (!nextFocusEle) {
138 | nextFocusEle = (this as HTMLElement)
139 | ?.lastElementChild as HTMLElement;
140 | }
141 | } else if (key === "ArrowDown" || key === "ArrowRight") {
142 | nextFocusEle = focusEle?.nextElementSibling as HTMLElement;
143 | if (!nextFocusEle) {
144 | nextFocusEle = (this as HTMLElement)
145 | ?.firstElementChild as HTMLElement;
146 | }
147 | }
148 | nextFocusEle?.focus();
149 | }
150 | };
151 | }
152 |
153 |
154 | hide = function () {
155 | this.menu.addClass("display-none");
156 | this.scrollArea?.removeClass("scroll-disable");
157 | };
158 |
159 | isVisible = function () {
160 | return !this.menu.hasClass("display-none");
161 | };
162 | remove = function () {
163 | this.scrollArea.removeChild(this.menu);
164 | };
165 | }
166 |
--------------------------------------------------------------------------------
/src/components/link-input-modal.ts:
--------------------------------------------------------------------------------
1 | import { App, Modal, Setting, Notice, debounce } from "obsidian";
2 | import { VALID_URL_REG } from "../constants";
3 |
4 | /**
5 | * url-input modal
6 | */
7 | export class InsertLinkModal extends Modal {
8 | linkUrl: string;
9 | isOpen: boolean;
10 | onSubmit: (linkUrl: string) => void;
11 |
12 | constructor(app: App, onSubmit: (linkUrl: string) => void) {
13 | super(app);
14 | this.onSubmit = onSubmit;
15 | this.isOpen = false;
16 | }
17 |
18 | onOpen() {
19 | this.isOpen = true;
20 | const { contentEl } = this;
21 | this.linkUrl = "";
22 | contentEl.createEl("h1", { text: "Insert a link bookmark" });
23 |
24 | new Setting(contentEl)
25 | .setName("Link URL")
26 | .addText((text) =>
27 | text.setValue(this.linkUrl).onChange((value) => {
28 | this.linkUrl = value;
29 | })
30 | )
31 | .setClass("link-input");
32 |
33 | new Setting(contentEl).addButton((btn) =>
34 | btn
35 | .setButtonText("Insert")
36 | .setCta()
37 | .onClick(() => {
38 | this.checkUrl(this.linkUrl);
39 | })
40 | );
41 |
42 | this.containerEl.addEventListener("keydown", (evt) => {
43 | if (evt.key === "Enter") {
44 | this.checkUrl(this.linkUrl);
45 | }
46 | });
47 | }
48 |
49 | onClose() {
50 | const { contentEl } = this;
51 | contentEl.empty();
52 | this.isOpen = false;
53 | }
54 |
55 | checkUrl = debounce((url: string) => {
56 | if (VALID_URL_REG.test(url)) {
57 | this.close();
58 | this.onSubmit(url);
59 | } else {
60 | new Notice("Please input a valid url");
61 | }
62 | }, 10);
63 | }
64 |
--------------------------------------------------------------------------------
/src/components/plugin-setting.ts:
--------------------------------------------------------------------------------
1 | // import ExamplePlugin from "./main";
2 | import TypingAsstPlugin from "main";
3 | import { App, PluginSettingTab, Setting, setIcon } from "obsidian";
4 | import Sortable from "sortablejs";
5 | import { CMD_CONFIG, HEADING_MENU } from "src/constants";
6 |
7 | export type CMD_TYPE = (typeof HEADING_MENU)[number]
8 | export interface ExamplePluginSettings {
9 | showPlaceholder: boolean;
10 | disableSelectionMenu: boolean;
11 | cmdsSorting: CMD_TYPE[]
12 | }
13 | export class ExampleSettingTab extends PluginSettingTab {
14 | plugin: TypingAsstPlugin;
15 | hasChanged: boolean;
16 |
17 | constructor(app: App, plugin: TypingAsstPlugin) {
18 | super(app, plugin);
19 | this.plugin = plugin;
20 | this.hasChanged = false;
21 | }
22 |
23 | display(): void {
24 | const { containerEl } = this;
25 |
26 | containerEl.empty();
27 |
28 | containerEl.createEl("h2", { text: "Typing Assistant" });
29 |
30 | containerEl.createEl("p", { text: "For any questions or suggestions during use, please feel free to " }).createEl("a", {
31 | text: "contact me",
32 | href: "https://github.com/Jambo2018/notion-assistant-plugin",
33 | });
34 |
35 | new Setting(containerEl)
36 | .setName("Typing Placeholder")
37 | .setDesc("Show \"💡Please input ‘ / ’ for more commands...\" prompt when typing on a blank line")
38 | .addToggle((component) =>
39 | component
40 | .setValue(this.plugin.settings.showPlaceholder)
41 | .onChange(async (value) => {
42 | this.hasChanged = true;
43 | this.plugin.settings.showPlaceholder = value;
44 | await this.plugin.saveSettings();
45 | })
46 | );
47 |
48 | new Setting(containerEl)
49 | .setName("Selection Options")
50 | .setDesc("Display shortcut options after selecting text")
51 | .addToggle((component) =>
52 | component
53 | .setValue(!this.plugin.settings.disableSelectionMenu)
54 | .onChange(async (value) => {
55 | this.plugin.settings.disableSelectionMenu = !value;
56 | await this.plugin.saveSettings();
57 | })
58 | );
59 |
60 |
61 | new Setting(containerEl)
62 | .setName("Commands Menu")
63 | .setDesc("Supports custom command combinations and drag-and-drop sorting; please ensure that at least 5 commands are open")
64 |
65 | const CmdSettings = containerEl.createDiv({ cls: "heading-config" })
66 |
67 | const CmdsOn = containerEl.createDiv({ attr: { id: 'cmds-on' }, cls: "heading-config-on" });
68 | const CmdsOff = containerEl.createDiv({ attr: { id: 'cmds-off' }, cls: "heading-config-off" });
69 |
70 | CmdSettings.appendChild(CmdsOn)
71 | CmdSettings.appendChild(CmdsOff)
72 |
73 | let cmdsSorting = this.plugin.settings?.cmdsSorting || []
74 | const cmdsAll = [...new Set([...cmdsSorting, ...HEADING_MENU])]
75 | for (let i = 0; i < cmdsAll.length; i++) {
76 | const cmd = cmdsAll[i]
77 | let HeaderItem: HTMLDivElement;
78 | const isChecked = this.plugin.settings.cmdsSorting.includes(cmd)
79 | if (isChecked) {
80 | HeaderItem = CmdsOn.createDiv({ cls: 'heading-item' })
81 | } else {
82 | HeaderItem = CmdsOff.createDiv({ cls: 'heading-item' })
83 | }
84 | const IconDiv = HeaderItem.createDiv({ cls: 'heading-item-icon' })
85 | setIcon(IconDiv, CMD_CONFIG[cmd].icon);
86 | new Setting(HeaderItem)
87 | .setName(CMD_CONFIG[cmd].title)
88 | .addToggle((component) =>
89 | component.setValue(isChecked)
90 | .onChange(async () => {
91 | if (cmdsSorting.includes(cmd)) {
92 | cmdsSorting = cmdsSorting.filter(i => i !== cmd)
93 | CmdsOn.removeChild(HeaderItem)
94 | CmdsOff.insertBefore(HeaderItem, CmdsOff.firstElementChild)
95 | } else {
96 | cmdsSorting.push(cmd)
97 | CmdsOff.removeChild(HeaderItem)
98 | CmdsOn.appendChild(HeaderItem)
99 | }
100 | this.hasChanged = true;
101 | this.plugin.settings.cmdsSorting = [...cmdsSorting]
102 | await this.plugin.saveSettings();
103 | })
104 | ).setDisabled(cmdsSorting.length <= 5 && cmdsSorting.includes(cmd))
105 | }
106 |
107 | new Sortable(CmdsOn, {
108 | onEnd: async (e) => {
109 | const cmdsSorting = Array.from(e.to.children).map(item => {
110 | return HEADING_MENU.find(cmd => CMD_CONFIG[cmd].title === item.textContent)
111 | })
112 | this.plugin.settings.cmdsSorting = [...cmdsSorting] as CMD_TYPE[]
113 | this.hasChanged = true;
114 | await this.plugin.saveSettings();
115 | },
116 | })
117 | }
118 |
119 | hide() {
120 | if (this.hasChanged) {
121 | (this.app as any).commands.executeCommandById("app:reload");
122 | }
123 | }
124 | }
--------------------------------------------------------------------------------
/src/components/selection-menu.ts:
--------------------------------------------------------------------------------
1 | import { setIcon } from "obsidian";
2 | import {
3 | CMD_CONFIG,
4 | HEADING_CMDS,
5 | MENU_MARGIN,
6 | MENU_WIDTH,
7 | SELECTION_CMDS,
8 | TEXT_MAP,
9 | } from "../constants";
10 |
11 | /**
12 | * the menu visible while text selected
13 | */
14 | export class SelectionBtns {
15 | menu: HTMLDivElement;
16 | lineMenu: HTMLDivElement;
17 | headerHeight: number;
18 | scrollArea?: HTMLDivElement;
19 | constructor(props: {
20 | scrollArea?: Element;
21 | headerHeight: number;
22 | onAction: (content: string, isHeading: boolean) => void;
23 | }) {
24 | this.menu = createDiv({
25 | cls: "selection",
26 | attr: { id: "selection-menu" },
27 | });
28 | this.scrollArea = props.scrollArea as HTMLDivElement;
29 | this.headerHeight = props.headerHeight;
30 | const _this = this;
31 | const menu_content = createDiv({ cls: "selection-content" });
32 | SELECTION_CMDS.forEach((item, idx) => {
33 | const btn = createDiv({
34 | cls: "selection-btn",
35 | attr: { commandType: idx },
36 | });
37 | if (idx === 0) {
38 | btn.createSpan(TEXT_MAP["text"]);
39 | } else {
40 | setIcon(btn, (CMD_CONFIG as any)[item].icon);
41 | }
42 | btn.onclick = function (e) {
43 | if (idx === 0) {
44 | e.preventDefault();
45 | e.stopPropagation();
46 | _this.showHeading();
47 | } else {
48 | props.onAction(item, false);
49 | }
50 | };
51 | menu_content.appendChild(btn);
52 | });
53 |
54 | this.menu.appendChild(menu_content);
55 | this.lineMenu = createDiv({ cls: "linemenu" });
56 | this.hideHeading();
57 | HEADING_CMDS.forEach((item, idx) => {
58 | const btn = createDiv({
59 | cls: "linemenu-option",
60 | attr: { commandType: idx },
61 | });
62 | const IconDiv = createDiv({
63 | parent: btn,
64 | cls: "linemenu-option-svg",
65 | });
66 | setIcon(IconDiv, CMD_CONFIG[item].icon);
67 | btn.createSpan({ text: CMD_CONFIG[item].title });
68 | btn.onclick = function () {
69 | props.onAction(item, true);
70 | };
71 | this.lineMenu.appendChild(btn);
72 | });
73 |
74 | this.menu.appendChild(this.lineMenu);
75 | this.scrollArea.appendChild(this.menu);
76 | this.hide();
77 | }
78 | display = function (lineStyleText: string) {
79 | const range = window?.getSelection()?.getRangeAt(0);
80 | const rect = range?.getBoundingClientRect();
81 | const scroll = this.scrollArea.getBoundingClientRect();
82 | if (!rect) return;
83 | let { height, top, left } = rect;
84 | top += MENU_MARGIN + height + this.scrollArea.scrollTop - scroll.top;
85 | left -= scroll.left;
86 |
87 | const upDis =
88 | top + 56 - this.scrollArea.scrollTop - this.scrollArea.clientHeight;
89 | if (upDis > 0) {
90 | this.scrollArea.scrollTo(0, this.scrollArea.scrollTop + upDis);
91 | }
92 |
93 | const rightDis = left + MENU_WIDTH - this.scrollArea.clientWidth;
94 | if (rightDis > 0) {
95 | left -= rightDis;
96 | }
97 | this.menu.firstElementChild.firstElementChild.textContent =
98 | lineStyleText;
99 | if (top < this.headerHeight + 16) {
100 | top = -999;
101 | }
102 | this.menu.style = `top:${top}px;left:${left}px`;
103 | this.menu.removeClass("display-none");
104 | this.menu.children[0].focus();
105 | };
106 | hide = function () {
107 | this.menu.addClass("display-none");
108 | this.hideHeading();
109 | };
110 | showHeading = function () {
111 | const rect = this.menu?.getBoundingClientRect();
112 | const contentRect = this.scrollArea.getBoundingClientRect();
113 | const topOffset =
114 | rect.top +
115 | rect.height +
116 | 430 -
117 | this.scrollArea.clientHeight -
118 | contentRect.top +
119 | MENU_MARGIN;
120 | const containerTopBorder = topOffset <= 0 ? 36 : 36 - topOffset;
121 | this.lineMenu.style = `top:${containerTopBorder}px`;
122 | };
123 | hideHeading = function () {
124 | this.lineMenu.style = "display:none";
125 | };
126 | isVisible = function () {
127 | return !!document.querySelector(".selection");
128 | };
129 | remove = function () {
130 | this.scrollArea.removeChild(this.menu);
131 | };
132 | }
133 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const ICON_MAP = {
2 | ['text']: '',
3 | ['heading1']: '',
4 | ['heading2']: '',
5 | ['heading3']: '',
6 | ['heading4']: '',
7 | ['heading5']: '',
8 | ['heading6']: '',
9 | ['todoList']: '',
10 | ['linkBookMark']: '',
11 | ['bulletList']: '',
12 | ['numberList']: '',
13 | ['bold']: '',
14 | ['strikethrough']: '',
15 | ['italics']: '',
16 | ['underline']: '',
17 | ['code']: '',
18 | ['divide']: '',
19 | ['quote']: '',
20 | ['link']: '',
21 | ['math']: '',
22 | ['highlight']: '',
23 | ["note-call-out"]: '',
24 | ["abstract-call-out"]: '',
25 | ["info-call-out"]: '',
26 | ["todo-call-out"]: '',
27 | ["tip-call-out"]: '',
28 | ["success-call-out"]: '',
29 | ["question-call-out"]: '',
30 | ["warning-call-out"]: '',
31 | ["failure-call-out"]: '',
32 | ["danger-call-out"]: '',
33 | ["bug-call-out"]: '',
34 | ["example-call-out"]: '',
35 | ['tag']: '',
36 | ['embed']: '',
37 | ['table']: ''
38 | }
39 |
40 | export const TEXT_MAP = {
41 | ['text']: "Text",
42 | ['heading1']: "Heading1",
43 | ['heading2']: "Heading2",
44 | ['heading3']: "Heading3",
45 | ['heading4']: "Heading4",
46 | ['heading5']: "Heading5",
47 | ['heading6']: "Heading6",
48 | ['todoList']: "To-do List",
49 | ['bulletList']: "Bulleted List",
50 | ['numberList']: "Numbered List",
51 | ['code']: "Code",
52 | ['quote']: "Quote",
53 | ['linkBookMark']: "Link Bookmark",
54 | ['divide']: "Divide",
55 | ["noteCallout"]: "Note Callout",
56 | ["abstractCallout"]: "Abstract Callout",
57 | ["infoCallout"]: "Info Callout",
58 | ["todoCallout"]: "Todo Callout",
59 | ["tipCallout"]: "Tip Callout",
60 | ["successCallout"]: "Success Callout",
61 | ["questionCallout"]: "Question Callout",
62 | ["warningCallout"]: "Warning Callout",
63 | ["failureCallout"]: "Failure Callout",
64 | ["dangerCallout"]: "Danger Callout",
65 | ["bugCallout"]: "Bug Callout",
66 | ["exampleCallout"]: "example Callout",
67 | };
68 |
69 | export const CMD_INSERTIONS = {
70 | ['insert-callout']: "callout",
71 | ['insert-tag']: "tag",
72 | ['insert-embed']: "embed",
73 | ['insert-math']: "math",
74 | ['insert-text']: 'text',
75 | ['insert-heading1']: 'heading1',
76 | ['insert-heading2']: 'heading2',
77 | ['insert-heading3']: 'heading3',
78 | ['insert-heading4']: 'heading4',
79 | ['insert-heading5']: 'heading5',
80 | ['insert-heading6']: 'heading6',
81 | ['insert-todo']: 'todoList',
82 | ['insert-bulletList']: 'bulletList',
83 | ['insert-numberList']: 'numberList',
84 | ['insert-divide']: 'divide',
85 | ['insert-quote']: 'quote',
86 | ['insert-code']: 'code',
87 | ['math']: 'math',
88 | ['highlight']: 'highlight'
89 | }
90 |
91 | export const CONTENT_MAP = {
92 | embed: "![[]]",
93 | noteCallout: "> [!NOTE]\n> ",
94 | abstractCallout: "> [!abstract]\n> ",
95 | infoCallout: "> [!info]\n> ",
96 | todoCallout: "> [!todo]\n> ",
97 | tipCallout: "> [!tip]\n> ",
98 | successCallout: "> [!success]\n> ",
99 | questionCallout: "> [!question]\n> ",
100 | warningCallout: "> [!warning]\n> ",
101 | failureCallout: "> [!failure]\n> ",
102 | dangerCallout: "> [!danger]\n> ",
103 | bugCallout: "> [!bug]\n> ",
104 | exampleCallout: "> [!example]\n> ",
105 | quote: "> ",
106 | linkBookMark: "bookmark",
107 | math: "$$\n\n$$",
108 | text: "",
109 | heading1: "# ",
110 | heading2: "## ",
111 | heading3: "### ",
112 | heading4: "#### ",
113 | heading5: "##### ",
114 | heading6: "###### ",
115 | todoList: "- [ ] ",
116 | bulletList: "- ",
117 | numberList: "1. ",
118 | divide: "***\n",
119 | code: "```\n\n```",
120 | bookmark:'bookmark',
121 | };
122 |
123 | export const HEADING_MENU = [
124 | 'insert-text',
125 | "insert-note-callout",
126 | "insert-abstract-callout",
127 | "insert-info-callout",
128 | "insert-todo-callout",
129 | "insert-tip-callout",
130 | "insert-success-callout",
131 | "insert-question-callout",
132 | "insert-warning-callout",
133 | "insert-failure-callout",
134 | "insert-danger-callout",
135 | "insert-bug-callout",
136 | "insert-example-callout",
137 | 'insert-heading1',
138 | 'insert-heading2',
139 | 'insert-heading3',
140 | 'insert-heading4',
141 | 'insert-heading5',
142 | 'insert-heading6',
143 | 'insert-tag',
144 | 'insert-math',
145 | 'insert-quote',
146 | 'insert-embed',
147 | "bookmark",
148 | 'insert-todo',
149 | 'insert-bulletList',
150 | 'insert-numberList',
151 | 'insert-code',
152 | 'insert-divide',
153 | 'insert-table',
154 | ] as const;
155 |
156 | export const HEADING_CMDS = [
157 | "set-text",
158 | "set-heading1",
159 | "set-heading2",
160 | "set-heading3",
161 | "set-heading4",
162 | "set-heading5",
163 | "set-heading6",
164 | "set-todo",
165 | "set-bulletList",
166 | "set-numberList",
167 | ] as const;
168 |
169 | export const SELECTION_CMDS = [
170 | "heading",
171 | "set-link",
172 | "toggle-bold",
173 | "toggle-strikethrough",
174 | "toggle-italics",
175 | "toggle-underline",
176 | "toggle-code",
177 | "toggle-math",
178 | "toggle-highlight"
179 | ] as const;
180 |
181 | export const CMD_CONFIG = {
182 | ["insert-note-callout"]: {
183 | title: "Note Callout",
184 | icon: "note-call-out",
185 | cmd: "typing-assistant:insert-note-callout"
186 | },
187 | ["insert-abstract-callout"]: {
188 | title: "Abstract Callout",
189 | icon: "abstract-call-out",
190 | cmd: "typing-assistant:insert-abstract-callout"
191 | },
192 | ["insert-info-callout"]: {
193 | title: "Info Callout",
194 | icon: "info-call-out",
195 | cmd: "typing-assistant:insert-info-callout"
196 | },
197 | ["insert-todo-callout"]: {
198 | title: "Todo Callout",
199 | icon: "todo-call-out",
200 | cmd: "typing-assistant:insert-todo-callout"
201 | },
202 | ["insert-tip-callout"]: {
203 | title: "Tip Callout",
204 | icon: "tip-call-out",
205 | cmd: "typing-assistant:insert-tip-callout"
206 | },
207 | ["insert-success-callout"]: {
208 | title: "Success Callout",
209 | icon: "success-call-out",
210 | cmd: "typing-assistant:insert-success-callout"
211 | },
212 | ["insert-question-callout"]: {
213 | title: "Question Callout",
214 | icon: "question-call-out",
215 | cmd: "typing-assistant:insert-question-callout"
216 | },
217 | ["insert-warning-callout"]: {
218 | title: "Warning Callout",
219 | icon: "warning-call-out",
220 | cmd: "typing-assistant:insert-warning-callout"
221 | },
222 | ["insert-failure-callout"]: {
223 | title: "Failure Callout",
224 | icon: "failure-call-out",
225 | cmd: "typing-assistant:insert-failure-callout"
226 | },
227 | ["insert-danger-callout"]: {
228 | title: "Danger Callout",
229 | icon: "danger-call-out",
230 | cmd: "typing-assistant:insert-danger-callout"
231 | },
232 | ["insert-bug-callout"]: {
233 | title: "Bug Callout",
234 | icon: "bug-call-out",
235 | cmd: "typing-assistant:insert-bug-callout"
236 | },
237 | ["insert-example-callout"]: {
238 | title: "Example Callout",
239 | icon: "example-call-out",
240 | cmd: "typing-assistant:insert-example-callout"
241 | },
242 | 'insert-tag': {
243 | title: 'Tag',
244 | icon: 'tag',
245 | cmd: "typing-assistant:insert-tag"
246 | },
247 | 'insert-quote': {
248 | title: 'Quote',
249 | icon: 'quote',
250 | cmd: "typing-assistant:insert-quote"
251 | },
252 | 'insert-math': {
253 | title: 'Math Block',
254 | icon: 'math',
255 | cmd: "typing-assistant:insert-mathblock"
256 | },
257 | 'insert-embed': {
258 | title: 'Embed',
259 | icon: 'embed',
260 | cmd: "typing-assistant:insert-embed"
261 | },
262 | 'insert-text': {
263 | title: 'Text',
264 | icon: 'text',
265 | cmd: 'typing-assistant:insert-text'
266 | },
267 | 'insert-heading1': {
268 | title: 'Heading1',
269 | icon: 'heading1',
270 | cmd: 'typing-assistant:insert-heading1'
271 | },
272 | 'insert-heading2': {
273 | title: 'Heading2',
274 | icon: 'heading2',
275 | cmd: 'typing-assistant:insert-heading2'
276 | },
277 | 'insert-heading3': {
278 | title: 'Heading3',
279 | icon: 'heading3',
280 | cmd: 'typing-assistant:insert-heading3'
281 | },
282 | 'insert-heading4': {
283 | title: 'Heading4',
284 | icon: 'heading4',
285 | cmd: 'typing-assistant:insert-heading4'
286 | },
287 | 'insert-heading5': {
288 | title: 'Heading5',
289 | icon: 'heading5',
290 | cmd: 'typing-assistant:insert-heading5'
291 | },
292 | 'insert-heading6': {
293 | title: 'Heading6',
294 | icon: 'heading6',
295 | cmd: 'typing-assistant:insert-heading6'
296 | },
297 | 'insert-bookmark': {
298 | title: 'BookMark',
299 | icon: 'bookmark',
300 | cmd: 'typing-assistant:insert-bookmark'
301 | },
302 | 'insert-todo': {
303 | title: 'To-do List',
304 | icon: 'todoList',
305 | cmd: 'typing-assistant:insert-todo'
306 | },
307 | 'insert-bulletList': {
308 | title: 'BulletList',
309 | icon: 'bulletList',
310 | cmd: 'typing-assistant:insert-bulletList'
311 | },
312 | 'insert-numberList': {
313 | title: 'NumberList',
314 | icon: 'numberList',
315 | cmd: 'typing-assistant:insert-numberList'
316 | },
317 | 'insert-divide': {
318 | title: 'Divide',
319 | icon: 'divide',
320 | cmd: 'typing-assistant:insert-divide'
321 | },
322 | 'insert-code': {
323 | title: 'Code',
324 | icon: 'code',
325 | cmd: 'typing-assistant:insert-codeblock'
326 | },
327 | 'toggle-math': {
328 | title: 'Math',
329 | icon: 'math',
330 | cmd: 'editor:toggle-inline-math'
331 | },
332 | 'toggle-highlight': {
333 | title: 'Highlight',
334 | icon: 'highlight',
335 | cmd: 'editor:toggle-highlight'
336 | },
337 |
338 | "bookmark": {
339 | title: 'Link BookMark',
340 | icon: 'link',
341 | cmd: 'typing-assistant:insert-bookmark'
342 | },
343 |
344 | 'set-text': {
345 | title: 'Text',
346 | icon: 'text',
347 | cmd: 'editor:set-heading-0'
348 | },
349 | 'set-heading1': {
350 | title: 'Heading1',
351 | icon: 'heading1',
352 | cmd: 'editor:set-heading-1'
353 | },
354 | 'set-heading2': {
355 | title: 'Heading2',
356 | icon: 'heading2',
357 | cmd: 'editor:set-heading-2'
358 | },
359 | 'set-heading3': {
360 | title: 'Heading3',
361 | icon: 'heading3',
362 | cmd: 'editor:set-heading-3'
363 | },
364 | 'set-heading4': {
365 | title: 'Heading4',
366 | icon: 'heading4',
367 | cmd: 'editor:set-heading-4'
368 | },
369 | 'set-heading5': {
370 | title: 'Heading5',
371 | icon: 'heading5',
372 | cmd: 'editor:set-heading-5'
373 | },
374 | 'set-heading6': {
375 | title: 'Heading6',
376 | icon: 'heading6',
377 | cmd: 'editor:set-heading-6'
378 | },
379 | 'set-todo': {
380 | title: 'To-do List',
381 | icon: 'todoList',
382 | cmd: "typing-assistant:todo-list"
383 | },
384 | 'set-bulletList': {
385 | title: 'BulletList',
386 | icon: 'bulletList',
387 | cmd: "editor:toggle-bullet-list"
388 | },
389 | 'set-numberList': {
390 | title: 'NumberList',
391 | icon: 'numberList',
392 | cmd: "editor:toggle-numbered-list",
393 | },
394 | 'set-link': {
395 | title: 'Link',
396 | icon: 'link',
397 | cmd: 'editor:insert-link'
398 | },
399 | 'toggle-bold': {
400 | title: 'Bold',
401 | icon: 'bold',
402 | cmd: "editor:toggle-bold",
403 | },
404 | 'toggle-strikethrough': {
405 | title: 'Strikethrough',
406 | icon: 'strikethrough',
407 | cmd: "editor:toggle-strikethrough",
408 | },
409 | 'toggle-italics': {
410 | title: 'Italics',
411 | icon: 'italics',
412 | cmd: "editor:toggle-italics",
413 | },
414 | 'toggle-underline': {
415 | title: 'Underline',
416 | icon: 'underline',
417 | cmd: "typing-assistant:underline",
418 | },
419 | 'toggle-code': {
420 | title: 'Code',
421 | icon: 'code',
422 | cmd: "editor:toggle-code",
423 | },
424 | 'insert-table': {
425 | title: 'Table',
426 | icon: 'table',
427 | cmd: "editor:insert-table",
428 | },
429 | } as const;
430 |
431 | const config2Arr = (conf: any) => {
432 | const arr = []
433 | for (const key in conf) {
434 | arr.push({ cmd: key, title: conf[key].title })
435 | }
436 | return arr
437 | }
438 | export let CMD_CONFIG_ARR = config2Arr(CMD_CONFIG)
439 |
440 | export const CODE_LAN = "link-bookmark";
441 | export const MENU_WIDTH = 300;
442 | export const MAX_MENU_HEIGHT = 400;
443 | export const COMMAD_ITEM_EIGHT = 46;
444 | export const MENU_MARGIN = 6;
445 | export const VALID_URL_REG =
446 | /^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/;
447 |
--------------------------------------------------------------------------------
/src/util/cmd-generate.ts:
--------------------------------------------------------------------------------
1 | import { Editor, MarkdownView } from "obsidian";
2 | import { CONTENT_MAP } from "src/constants";
3 |
4 | /**
5 | * create customized cmds
6 | */
7 | export function loadCommands() {
8 |
9 | const formatUnderline = (
10 | editor: Editor,
11 | line: number,
12 | left: number,
13 | right: number
14 | ) => {
15 | // range of selected content
16 | const selectedRange = [
17 | { line, ch: left },
18 | { line, ch: right },
19 | ] as const;
20 | let selection = editor.getRange(...selectedRange);
21 | if (/((?!u>).*?)(((?!u>).*<\/u>)+)((?!u>).*?)/.test(selection)) {
22 | selection = selection.replace(/<\/?u>/g, "");
23 | }
24 | editor.replaceRange(`${selection}`, ...selectedRange);
25 | const content = editor.getLine(line);
26 | const arr = content.split(/<\/?u>/g);
27 | let joinContent = "";
28 | arr.forEach((item, index) => {
29 | if (index % 2 === 0) {
30 | joinContent += item ?? "";
31 | if (index < arr.length - 1) {
32 | joinContent += "";
33 | }
34 | } else {
35 | joinContent += (item ?? "") + "";
36 | }
37 | });
38 | joinContent = joinContent.replace(/(<\/u>)|(<\/u>)/g, "");
39 | editor.setLine(line, joinContent);
40 | };
41 |
42 |
43 |
44 | const generateCommand = (content: string) => {
45 | const view = this.app.workspace.getActiveViewOfType(MarkdownView)
46 | if (view) {
47 | if (content === CONTENT_MAP['bookmark']) {
48 | content = ''
49 | }
50 | const cursor = view.editor.getCursor();
51 | const editLine = view.editor.getLine(cursor.line);
52 | const searchText = editLine.match(/[^\/]*$/)?.[0] ?? ''
53 | if (editLine.length > searchText.length + 1) {
54 | view.editor.replaceRange(
55 | `\n${content}`,
56 | { line: cursor.line, ch: cursor.ch - searchText.length - 1 },
57 | cursor
58 | );
59 | view.editor.setCursor({
60 | line: cursor.line + 1,
61 | ch: content.length,
62 | });
63 | } else {
64 | view.editor.setLine(cursor.line, content);
65 | view.editor.setCursor({
66 | line: cursor.line,
67 | ch: content.length,
68 | });
69 | }
70 | view.editor.focus();
71 | }
72 | }
73 |
74 | this.addCommand({
75 | id: "insert-text",
76 | name: "Insert normal text",
77 | editorCallback: (editor: Editor) => {
78 | generateCommand(CONTENT_MAP['text'])
79 | },
80 | });
81 |
82 | this.addCommand({
83 | id: "insert-heading1",
84 | name: "Insert Heading-1",
85 | editorCallback: (editor: Editor) => {
86 | generateCommand(CONTENT_MAP['heading1'])
87 | },
88 | });
89 | this.addCommand({
90 | id: "insert-heading2",
91 | name: "Insert Heading-2",
92 | editorCallback: (editor: Editor) => {
93 | generateCommand(CONTENT_MAP['heading2'])
94 | },
95 | });
96 | this.addCommand({
97 | id: "insert-heading3",
98 | name: "Insert Heading-3",
99 | editorCallback: (editor: Editor) => {
100 | generateCommand(CONTENT_MAP['heading3'])
101 | },
102 | });
103 | this.addCommand({
104 | id: "insert-heading4",
105 | name: "Insert Heading-4",
106 | editorCallback: (editor: Editor) => {
107 | generateCommand(CONTENT_MAP['heading4'])
108 | },
109 | });
110 | this.addCommand({
111 | id: "insert-heading5",
112 | name: "Insert Heading-5",
113 | editorCallback: (editor: Editor) => {
114 | generateCommand(CONTENT_MAP['heading5'])
115 | },
116 | });
117 | this.addCommand({
118 | id: "insert-heading6",
119 | name: "Insert Heading-6",
120 | editorCallback: (editor: Editor) => {
121 | generateCommand(CONTENT_MAP['heading6'])
122 | },
123 | });
124 | this.addCommand({
125 | id: "insert-todo",
126 | name: "Insert TodoList",
127 | editorCallback: (editor: Editor) => {
128 | generateCommand(CONTENT_MAP['todoList'])
129 | },
130 | });
131 | this.addCommand({
132 | id: "insert-bulletList",
133 | name: "Insert BulletList",
134 | editorCallback: (editor: Editor) => {
135 | generateCommand(CONTENT_MAP['bulletList'])
136 | },
137 | });
138 | this.addCommand({
139 | id: "insert-numberList",
140 | name: "Insert NumberList",
141 | editorCallback: (editor: Editor) => {
142 | generateCommand(CONTENT_MAP['numberList'])
143 | },
144 | });
145 | this.addCommand({
146 | id: "insert-bookmark",
147 | name: "Insert BookMark",
148 | editorCallback: (editor: Editor) => {
149 | generateCommand(CONTENT_MAP['bookmark'])
150 | },
151 | });
152 | this.addCommand({
153 | id: "insert-divide",
154 | name: "Insert Divide",
155 | editorCallback: (editor: Editor) => {
156 | generateCommand(CONTENT_MAP['divide'])
157 | },
158 | });
159 | this.addCommand({
160 | id: "insert-quote",
161 | name: "Insert Quote",
162 | editorCallback: (editor: Editor) => {
163 | generateCommand(CONTENT_MAP['quote'])
164 | },
165 | });
166 | this.addCommand({
167 | id: "insert-note-callout",
168 | name: "Insert Callout",
169 | editorCallback: (editor: Editor) => {
170 | generateCommand(CONTENT_MAP["noteCallout"]);
171 | }
172 | });
173 | this.addCommand({
174 | id: "insert-abstract-callout",
175 | name: "Insert Callout",
176 | editorCallback: (editor: Editor) => {
177 | generateCommand(CONTENT_MAP["abstractCallout"]);
178 | }
179 | });
180 | this.addCommand({
181 | id: "insert-info-callout",
182 | name: "Insert Callout",
183 | editorCallback: (editor: Editor) => {
184 | generateCommand(CONTENT_MAP["infoCallout"]);
185 | }
186 | });
187 | this.addCommand({
188 | id: "insert-todo-callout",
189 | name: "Insert Callout",
190 | editorCallback: (editor: Editor) => {
191 | generateCommand(CONTENT_MAP["todoCallout"]);
192 | }
193 | });
194 | this.addCommand({
195 | id: "insert-tip-callout",
196 | name: "Insert Callout",
197 | editorCallback: (editor: Editor) => {
198 | generateCommand(CONTENT_MAP["tipCallout"]);
199 | }
200 | });
201 | this.addCommand({
202 | id: "insert-success-callout",
203 | name: "Success Callout",
204 | editorCallback: (editor: Editor) => {
205 | generateCommand(CONTENT_MAP["successCallout"]);
206 | }
207 | });
208 | this.addCommand({
209 | id: "insert-question-callout",
210 | name: "Question Callout",
211 | editorCallback: (editor: Editor) => {
212 | generateCommand(CONTENT_MAP["questionCallout"]);
213 | }
214 | });
215 | this.addCommand({
216 | id: "insert-warning-callout",
217 | name: "Warning Callout",
218 | editorCallback: (editor: Editor) => {
219 | generateCommand(CONTENT_MAP["warningCallout"]);
220 | }
221 | });
222 | this.addCommand({
223 | id: "insert-failure-callout",
224 | name: "Failure Callout",
225 | editorCallback: (editor: Editor) => {
226 | generateCommand(CONTENT_MAP["failureCallout"]);
227 | }
228 | });
229 | this.addCommand({
230 | id: "insert-danger-callout",
231 | name: "Danger Callout",
232 | editorCallback: (editor: Editor) => {
233 | generateCommand(CONTENT_MAP["dangerCallout"]);
234 | }
235 | });
236 | this.addCommand({
237 | id: "insert-bug-callout",
238 | name: "Bug Callout",
239 | editorCallback: (editor: Editor) => {
240 | generateCommand(CONTENT_MAP["bugCallout"]);
241 | }
242 | });
243 | this.addCommand({
244 | id: "insert-example-callout",
245 | name: "Example Callout",
246 | editorCallback: (editor: Editor) => {
247 | generateCommand(CONTENT_MAP["exampleCallout"]);
248 | }
249 | });
250 | this.addCommand({
251 | id: "insert-mathblock",
252 | name: "Insert Math Block",
253 | editorCallback: (editor: Editor) => {
254 | generateCommand(CONTENT_MAP['math'])
255 | CONTENT_MAP['code']
256 | const view = this.app.workspace.getActiveViewOfType(MarkdownView)
257 | if (view) {
258 | const cursor = view.editor.getCursor();
259 | const editLine = view.editor.getLine(cursor.line);
260 | view.editor.setCursor(cursor.line - 1)
261 | view.editor.focus();
262 | }
263 | },
264 | });
265 | this.addCommand({
266 | id: "insert-codeblock",
267 | name: "Insert Math Block",
268 | editorCallback: (editor: Editor) => {
269 | generateCommand(CONTENT_MAP['code'])
270 | const view = this.app.workspace.getActiveViewOfType(MarkdownView)
271 | if (view) {
272 | const cursor = view.editor.getCursor();
273 | const editLine = view.editor.getLine(cursor.line);
274 | view.editor.setCursor(cursor.line - 1)
275 | view.editor.focus();
276 | }
277 | },
278 | });
279 | this.addCommand({
280 | id: "insert-tag",
281 | name: "Insert Tag",
282 | editorCallback: (editor: Editor) => {
283 | const view = this.app.workspace.getActiveViewOfType(MarkdownView)
284 | if (view) {
285 | const cursor = view.editor.getCursor();
286 | const editLine = view.editor.getLine(cursor.line);
287 | let content = editLine.length > 1 ? " #" : '#'
288 | view.editor.replaceRange(
289 | content,
290 | { line: cursor.line, ch: cursor.ch - 1 },
291 | cursor
292 | );
293 | view.editor.focus();
294 | }
295 | },
296 | });
297 |
298 | this.addCommand({
299 | id: "insert-embed",
300 | name: "Insert Embed",
301 | editorCallback: (editor: Editor) => {
302 | generateCommand(CONTENT_MAP['embed'])
303 | const view = this.app.workspace.getActiveViewOfType(MarkdownView)
304 | if (view) {
305 | const cursor = view.editor.getCursor();
306 | view.editor.setCursor({ ...cursor, ch: cursor.ch - 2 })
307 | view.editor.focus();
308 | }
309 | },
310 | });
311 |
312 |
313 | // This adds a simple command that can be triggered anywhere
314 | this.addCommand({
315 | id: "underline",
316 | name: "Underline/Cancel underline",
317 | editorCallback: (editor: Editor) => {
318 | const from = editor.getCursor("from");
319 | const to = editor.getCursor("to");
320 | for (let i = from.line; i <= to.line; i++) {
321 | const len = editor.getLine(i).length;
322 | if (from.line === to.line) {
323 | formatUnderline(editor, i, from.ch, to.ch);
324 | } else if (i === from.line && i < to.line) {
325 | formatUnderline(editor, i, from.ch, len);
326 | } else if (i > from.line && i < to.line) {
327 | formatUnderline(editor, i, 0, len);
328 | } else if (i > from.line && i === to.line) {
329 | formatUnderline(editor, i, 0, to.ch);
330 | }
331 | }
332 | },
333 | });
334 | this.addCommand({
335 | id: "todo-list",
336 | name: "Add TodoList",
337 | editorCallback: (editor: Editor) => {
338 | const { line, ch } = editor.getCursor();
339 | const content = editor.getLine(line);
340 | if (content.startsWith("[ ] ")) {
341 | editor.replaceRange("", { line, ch: 0 }, { line, ch: 4 });
342 | } else {
343 | editor.replaceRange(
344 | `- [ ] `,
345 | { line, ch: 0 },
346 | { line, ch: 0 }
347 | );
348 | }
349 | },
350 | });
351 |
352 | }
353 |
--------------------------------------------------------------------------------
/src/util/link-bookmark.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * render a card by bookmark-md
3 | */
4 | export const generateBookMark = (content: string): HTMLDivElement => {
5 | const obj: Record = {};
6 | (content.split("\n") ?? []).forEach((item) => {
7 | const [str, key, value = ""] = item.match(/([^:]+):(.*)/) ?? [];
8 | obj[key] = value;
9 | });
10 | const linkDiv = createDiv();
11 | linkDiv.setAttribute("class", "ta-bookmark");
12 | linkDiv.onclick = () => {
13 | window.open(obj.url);
14 | };
15 | const contentDiv = createDiv({ parent: linkDiv, cls: "ta-bookmark-content" });
16 | if (obj.title) {
17 | contentDiv.createDiv({ cls: "ta-bookmark-title", text: obj.title });
18 | }
19 | if (obj.description) {
20 | contentDiv.createDiv({
21 | cls: "ta-bookmark-description",
22 | text: obj.description,
23 | });
24 | }
25 | const urlDiv = contentDiv.createDiv({ cls: "ta-bookmark-url" });
26 | if (obj.logo) {
27 | urlDiv.createDiv({
28 | cls: "ta-bookmark-url-logo",
29 | attr: { style: `background-image: url('${obj.logo}')` },
30 | });
31 | }
32 | urlDiv.createSpan({ cls: "ta-bookmark-url-text", text: obj.url });
33 | if(obj.coverImg){
34 | linkDiv.createDiv({cls:"ta-bookmark-cover",attr:{style:`background-image: url('${obj.coverImg}')`}})
35 | }
36 | return linkDiv;
37 | };
38 |
--------------------------------------------------------------------------------
/src/util/util.ts:
--------------------------------------------------------------------------------
1 | import { addIcon, request } from "obsidian";
2 | export * from './cmd-generate';
3 | export * from "./link-bookmark";
4 | export interface LinkResult {
5 | url: string;
6 | title?: string;
7 | logo?: string;
8 | description?: string;
9 | coverImg?: string;
10 | }
11 |
12 | const handleUrlPrefix = (link: string, url: string) => {
13 | if (/^\/\//.test(url)) {
14 | url = link.split(":")[0] + ":" + url;
15 | } else if (/^\/[^/]/.test(url)) {
16 | url = link.split("?")[0] + url;
17 | }
18 | return url;
19 | };
20 | export const linkParse = async (link: string): Promise => {
21 | const result: LinkResult = { url: link };
22 | try {
23 | const html = await request(link);
24 | if (html) {
25 | let titleMatch = html.match(
26 | /]*title[^>]*content="(.*?)"[^>]*>/
27 | );
28 | if (!titleMatch || titleMatch.length <= 1) {
29 | titleMatch = html.match(/]*>(.*?)<\/title>/);
30 | }
31 | result.title = titleMatch?.[1] ?? "";
32 |
33 | const desMatch = html.match(
34 | /]*description[^>]*content="(.*?)"[^>]*>/
35 | );
36 | result.description = desMatch?.[1] ?? "";
37 |
38 | let imgMatch = html.match(
39 | /]*image[^>]*content="(.*?)"[^>]*>/
40 | );
41 | if (!imgMatch || imgMatch.length <= 1) {
42 | imgMatch = html.match(/
]*src="(.*?)"[^>]*>/);
43 | }
44 | result.coverImg = imgMatch?.[1] ?? "";
45 | if (result.coverImg) {
46 | result.coverImg = handleUrlPrefix(link, result.coverImg);
47 | }
48 | const logoMatch = html.match(
49 | /]*icon[^>]*href="([^"]*)"[^>]*>/
50 | );
51 | result.logo = logoMatch?.[1] ?? "";
52 | if (result.logo) {
53 | result.logo = handleUrlPrefix(link, result.logo);
54 | }
55 | }
56 | return result;
57 | } catch (e) {
58 | console.warn("request link error:", e);
59 | return result;
60 | }
61 | };
62 |
63 | export const loadIcons = (icons: Record): void => {
64 | for (const key in icons) {
65 | addIcon(key, icons[key])
66 | }
67 | }
68 |
69 |
70 | export const isLineEdit = (el: any) => {
71 | return !!(el as any)?.querySelector(".cm-active:not(.HyperMD-codeblock)");
72 | }
73 |
74 | export const isLineSelect = (el: any) => {
75 | const arr = Array.from(el.classList);
76 | return !(arr.find(e => (e as string).endsWith('codeblock') || (e as string).endsWith('inline-title')));
77 | }
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | .theme-light {
2 | --empty-prompt: rgba(34, 34, 34, 0.35);
3 | --box-shadow: rgba(15, 15, 15, 0.05) 0px 0px 0px 1px,
4 | rgba(15, 15, 15, 0.1) 0px 3px 6px, rgba(15, 15, 15, 0.2) 0px 9px 24px;
5 | --bookmark-border: rgba(55, 53, 47, 0.16);
6 | --bookmark-title: #37352f;
7 | --bookmark-subtitle: rgb(55, 53, 47);
8 | }
9 | .theme-dark {
10 | --empty-prompt: rgba(218, 218, 218, 0.35);
11 | --box-shadow: rgba(150, 150, 150, 0.05) 0px 0px 0px 1px,
12 | rgba(150, 150, 150, 0.1) 0px 3px 6px,
13 | rgba(150, 150, 150, 0.2) 0px 9px 24px;
14 | --bookmark-border: rgba(255, 255, 255, 0.13);
15 | --bookmark-title: rgba(255, 255, 255, 0.81);
16 | --bookmark-subtitle: rgba(255, 255, 255, 0.443);
17 | }
18 | :root {
19 | --show-empty-prompt: none;
20 | }
21 |
22 | .command {
23 | position: absolute;
24 | z-index: 9;
25 | width: 240px;
26 | max-height: 400px;
27 | overflow-y: auto;
28 | background-color: var(--color-base-20);
29 | border-radius: 5px;
30 | box-shadow: var(--box-shadow);
31 | transform: 0 top;
32 | cursor: pointer;
33 | }
34 | .command::-webkit-scrollbar {
35 | display: none;
36 | }
37 |
38 | .command-option {
39 | display: flex;
40 | align-items: center;
41 | width: 100%;
42 | padding: 6px 12px;
43 | }
44 | .command-option:focus {
45 | background-color: var(--color-base-40);
46 | }
47 | .command-option div {
48 | display: flex;
49 | align-items: center;
50 | margin-right: 12px;
51 | padding: 8px;
52 | background-color: #f6f6f6;
53 | border-radius: 3px;
54 | box-shadow: var(--shadow-s);
55 | }
56 | .command-option svg {
57 | width: 18px;
58 | height: 18px;
59 | }
60 |
61 | .ta-bookmark {
62 | display: flex;
63 | box-sizing: border-box;
64 | width: 100%;
65 | overflow: hidden;
66 | border: 1px solid var(--bookmark-border);
67 | border-radius: 3px;
68 | cursor: pointer;
69 | }
70 | .ta-bookmark:hover {
71 | border: none;
72 | }
73 | .ta-bookmark-content {
74 | flex: 2;
75 | padding: 16px;
76 | overflow: hidden;
77 | }
78 | .ta-bookmark-title {
79 | min-height: 24px;
80 | margin-bottom: 2px;
81 | overflow: hidden;
82 | color: var(--bookmark-title);
83 | line-height: 20px;
84 | white-space: nowrap;
85 | text-overflow: ellipsis;
86 | }
87 | .ta-bookmark-description {
88 | height: 32px;
89 | overflow: hidden;
90 | color: var(--bookmark-subtitle);
91 | font-size: 12px;
92 | line-height: 16px;
93 | opacity: 0.65;
94 | }
95 | .ta-bookmark-url {
96 | display: flex;
97 | align-items: center;
98 | margin-top: 6px;
99 | }
100 | .ta-bookmark-url-logo {
101 | width: 16px;
102 | height: 16px;
103 | margin-right: 6px;
104 | background-repeat: no-repeat;
105 | background-position: center;
106 | background-size: contain;
107 | }
108 | .ta-bookmark-url-text {
109 | overflow: hidden;
110 | color: var(--bookmark-title);
111 | font-size: 12px;
112 | white-space: nowrap;
113 | text-overflow: ellipsis;
114 | }
115 | .ta-bookmark-cover {
116 | flex: 1;
117 | background-repeat: no-repeat;
118 | background-position: center;
119 | background-size: cover;
120 | }
121 | @media screen and (max-width: 400px) {
122 | .ta-bookmark-cover {
123 | display: none;
124 | }
125 | }
126 |
127 | .selection {
128 | position: absolute;
129 | z-index: 9;
130 | }
131 | .selection-content {
132 | display: flex;
133 | align-items: center;
134 | max-width: 100%;
135 | height: 32px;
136 | overflow: hidden;
137 | background-color: var(--color-base-20);
138 | border-radius: 5px;
139 | box-shadow: var(--box-shadow);
140 | }
141 | .selection-btn {
142 | display: flex;
143 | flex-direction: row;
144 | align-items: center;
145 | justify-content: center;
146 | height: 100%;
147 | padding: 0 6px;
148 | cursor: pointer;
149 | }
150 |
151 | .selection-btn:hover {
152 | background-color: var(--color-base-40);
153 | }
154 | .selection-btn:active {
155 | background-color: var(--color-base-40);
156 | }
157 |
158 | .selection-btn:first-child {
159 | padding: 0 10px;
160 | font-size: 14px;
161 | border-right: 1px solid var(--color-base-40);
162 | }
163 | .selection-btn:first-child::after {
164 | width: 15px;
165 | height: 15px;
166 | margin-left: 6px;
167 | background-image: url("data:image/png;base64,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");
168 | background-position: center;
169 | background-size: contain;
170 | content: "";
171 | }
172 |
173 | .selection-btn svg {
174 | width: 15px;
175 | height: 15px;
176 | }
177 | .selection-btn path {
178 | fill: var(--color-base-100);
179 | stroke: var(--color-base-100);
180 | }
181 |
182 | .linemenu {
183 | position: absolute;
184 | left: 0;
185 | overflow: hidden;
186 | background-color: var(--color-base-20);
187 | border-radius: 5px;
188 | box-shadow: var(--box-shadow);
189 | }
190 |
191 | .linemenu-option {
192 | display: flex;
193 | align-items: center;
194 | padding: 6px 12px;
195 | cursor: pointer;
196 | }
197 | .linemenu-option:hover {
198 | background-color: var(--color-base-40);
199 | }
200 | .linemenu-option:active {
201 | background-color: var(--color-base-40);
202 | }
203 | .linemenu-option div {
204 | display: flex;
205 | align-items: center;
206 | margin-right: 12px;
207 | padding: 8px;
208 | background-color: #f6f6f6;
209 | border-radius: 3px;
210 | box-shadow: var(--shadow-s);
211 | }
212 | .linemenu-option svg {
213 | width: 100%;
214 | height: 15px;
215 | }
216 |
217 |
218 | .cm-active:has(br):not(.HyperMD-codeblock)::before {
219 | display: var(--show-empty-prompt);
220 | position: absolute;
221 | top: 0;
222 | right: 0;
223 | bottom: 0;
224 | left: 3px;
225 | color: var(--empty-prompt);
226 | content: "💡Please input ‘ / ’ for more commands...";
227 | }
228 |
229 |
230 | .table-editor .cm-active:has(br)::before {
231 | display: none;
232 | }
233 |
234 | .link-input input {
235 | width: 100%;
236 | }
237 |
238 | .scroll-disable {
239 | overflow: hidden;
240 | }
241 |
242 | .display-none {
243 | display: none;
244 | }
245 |
246 | .heading-config {
247 | display: flex;
248 | flex-direction: column;
249 | max-width: 360px;
250 | margin: auto;
251 | overflow: hidden;
252 | border: 1px solid var(--background-modifier-border);
253 | border-radius: 6px;
254 | }
255 | .heading-config-on,
256 | .heading-config-off {
257 | display: flex;
258 | flex-direction: column;
259 | width: 100%;
260 | }
261 | .heading-config-on > div {
262 | cursor: all-scroll;
263 | }
264 | .heading-config-off > div {
265 | background-color: var(--background-secondary);
266 | }
267 |
268 | .heading-item {
269 | display: flex;
270 | flex-direction: row;
271 | gap: 12px;
272 | align-items: center;
273 | box-sizing: border-box;
274 | width: 100%;
275 | height: 48px;
276 | padding: 0 24px !important;
277 | border-bottom: 1px solid var(--background-modifier-border);
278 | .setting-item {
279 | width: 100%;
280 | border-top: none;
281 | }
282 | }
283 |
284 | .heading-item-icon {
285 | display: flex;
286 | align-items: center;
287 | margin-right: 12px;
288 | padding: 8px;
289 | background-color: #f6f6f6;
290 | border-radius: 3px;
291 | box-shadow: var(--shadow-s);
292 | }
293 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "inlineSourceMap": true,
5 | "inlineSources": true,
6 | "module": "ESNext",
7 | "target": "ES6",
8 | "allowJs": true,
9 | "noImplicitAny": true,
10 | "moduleResolution": "node",
11 | "importHelpers": true,
12 | "isolatedModules": true,
13 | "strictNullChecks": true,
14 | "allowSyntheticDefaultImports": true,
15 | "lib": ["DOM", "ES5", "ES6", "ES7"]
16 | },
17 | "include": ["**/*.ts"]
18 | }
19 |
--------------------------------------------------------------------------------
/version-bump.mjs:
--------------------------------------------------------------------------------
1 | import { readFileSync, writeFileSync } from "fs";
2 |
3 | const targetVersion = process.env.npm_package_version;
4 |
5 | // read minAppVersion from manifest.json and bump version to target version
6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
7 | const { minAppVersion } = manifest;
8 | manifest.version = targetVersion;
9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
10 |
11 | // update versions.json with target version and minAppVersion from manifest.json
12 | let versions = JSON.parse(readFileSync("versions.json", "utf8"));
13 | versions[targetVersion] = minAppVersion;
14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
15 |
--------------------------------------------------------------------------------
/versions.json:
--------------------------------------------------------------------------------
1 | {
2 | "1.0.0": "0.15.0"
3 | }
4 |
--------------------------------------------------------------------------------