├── .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 | ![GitHub all releases](https://img.shields.io/github/downloads/jambo2018/notion-assistant-plugin/total) 5 | 6 | ## Demo 1 7 | ![Typing Assistant Demo](/src/assets/demo.gif) 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 | --------------------------------------------------------------------------------