mirror of
https://github.com/gapmiss/inline-callouts.git
synced 2026-07-22 05:45:12 +00:00
Initial commit
This commit is contained in:
commit
77f3a98765
24 changed files with 1723 additions and 0 deletions
10
.editorconfig
Normal file
10
.editorconfig
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
tab_width = 4
|
||||
3
.eslintignore
Normal file
3
.eslintignore
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
|
||||
main.js
|
||||
23
.eslintrc
Normal file
23
.eslintrc
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"env": { "node": true },
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"@typescript-eslint/no-empty-function": "off"
|
||||
}
|
||||
}
|
||||
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# vscode
|
||||
.vscode
|
||||
|
||||
# Intellij
|
||||
*.iml
|
||||
.idea
|
||||
|
||||
# npm
|
||||
node_modules
|
||||
|
||||
# Don't include the compiled main.js file in the repo.
|
||||
# They should be uploaded to GitHub releases instead.
|
||||
dist/*
|
||||
|
||||
# Exclude sourcemaps
|
||||
*.map
|
||||
|
||||
# obsidian
|
||||
data.json
|
||||
|
||||
# Exclude macOS Finder (System Explorer) View States
|
||||
.DS_Store
|
||||
|
||||
.hotreload
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
build.sh
|
||||
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
tag-version-prefix=""
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 gapmiss
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
152
README.md
Normal file
152
README.md
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
# Inline Callouts
|
||||
|
||||
## Introduction
|
||||
|
||||
A plugin for displaying inline "callouts" in [Obsidian.md](https://github.com/obsidianmd), a successor to the [Badges](https://github.com/gapmiss/badges) plugin by the same author. Key differences: simpler syntax, access to all Lucide icons used by Obsidian, modal interfaces for icon suggestions, new inline callout and modify inline callout.
|
||||
|
||||
- [Features](#features)
|
||||
- [Usage](#usage)
|
||||
- [Caveats](#caveats)
|
||||
- [Installation](#installation)
|
||||
- [CSS styles](#css)
|
||||
- [Style Settings plugin](#style-settings-plugin)
|
||||
- [Development](#development)
|
||||
- [Credits](#credits)
|
||||
- [Notes](#notes)
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
## Usage
|
||||
|
||||
### Syntax
|
||||
|
||||
```markdown
|
||||
`[!!ICON|LABEL|COLOR]`
|
||||
```
|
||||
|
||||
| Syntax | Details |
|
||||
| ----------------- | ------------------------------ |
|
||||
| `ICON` | name of the Lucide icon |
|
||||
| `LABEL`(optional) | callout label/title text |
|
||||
| `COLOR`(optional) | RGB values or Obsidian CSS var |
|
||||
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The `LABEL` cannot contain either the `|` pipe or the <code>`</code> backtick symbols, as they are used as delimiters for the custom syntax.
|
||||
|
||||
## Caveats
|
||||
|
||||
- No extraneous `|` or <code>`</code> characters
|
||||
- Consecutive callouts must be separated by at lease one space, character, or line return to be rendered correctly in reading view.
|
||||
- Does not work inside a wiki-link
|
||||
- Markdown and HTML code is NOT rendered in the callout label
|
||||
- When using inline callouts in a table, the pipe characters must be escaped. e.g.:
|
||||
```
|
||||
| example one | example two |
|
||||
| ---------------------------------------- | ----------------------------- |
|
||||
| `[!!info\|Lorem\|var(--color-blue-rgb)]` | Lorem ipsum `[!!check-check]` |
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
From Obsidian's settings or preferences:
|
||||
|
||||
1. [Open in Obsidian.md](obsidian://show-plugin?id=inline-callouts)
|
||||
|
||||
or:
|
||||
|
||||
1. Community Plugins > Browse
|
||||
2. Search for "Badges"
|
||||
|
||||
Manually:
|
||||
|
||||
1. download the latest [release archive](https://github.com/gapmiss/inline-callouts/releases/download/0.1.0/inline-callouts-v0.1.0.zip)
|
||||
2. uncompress the downloaded archive
|
||||
3. move the `inline-callouts` folder to `/path/to/vault/.obsidian/plugins/`
|
||||
4. Settings > Community plugins > reload **Installed plugins**
|
||||
5. enable plugin
|
||||
|
||||
or:
|
||||
|
||||
1. download `main.js`, `manifest.json` & `styles.css`
|
||||
2. create a new folder `/path/to/vault/.obsidian/plugins/inline-callouts`
|
||||
3. move all 3 files to `/path/to/vault/.obsidian/plugins/inline-callouts`
|
||||
4. Settings > Community plugins > reload **Installed plugins**
|
||||
5. enable plugin
|
||||
|
||||
|
||||
## CSS
|
||||
|
||||
Custom `CSS` styles can be applied via CSS snippets. All colors and styles can be over-written just the same.
|
||||
|
||||
See [CSS snippets - Obsidian Help](https://help.obsidian.md/Extending+Obsidian/CSS+snippets)
|
||||
|
||||
### variables
|
||||
|
||||
```css
|
||||
body {
|
||||
--inline-callout-font-size: .85em;
|
||||
--inline-callout-font-weight: 400;
|
||||
--inline-callout-border-radius: 4px;
|
||||
--inline-callout-bg-transparency: .1;
|
||||
--inline-callout-margin: 1px;
|
||||
--inline-callout-padding-top: 1px;
|
||||
--inline-callout-padding-right: 0px;
|
||||
--inline-callout-padding-bottom: 2px;
|
||||
--inline-callout-padding-left: 0px;
|
||||
--inline-callout-icon-size: .85em;
|
||||
--inline-callout-icon-margin-top: 3px;
|
||||
--inline-callout-icon-margin-right: 3px;
|
||||
--inline-callout-icon-margin-bottom: 0px;
|
||||
--inline-callout-icon-margin-left: 5px;
|
||||
--inline-callout-label-margin-top: 2px;
|
||||
--inline-callout-label-margin-right: 5px;
|
||||
--inline-callout-label-margin-bottom: 0px;
|
||||
--inline-callout-label-margin-left: 2px;
|
||||
}
|
||||
```
|
||||
|
||||
## Style Settings plugin
|
||||
|
||||
All the above CSS variables can be modified via the [Style Settings plugin](https://github.com/mgmeyers/obsidian-style-settings)
|
||||
|
||||
## Development
|
||||
|
||||
### Clone this repo
|
||||
|
||||
```bash
|
||||
cd /path/to/vault/.obsidian/plugins
|
||||
git clone https://github.com/gapmiss/inline-callouts.git
|
||||
cd inline-callouts
|
||||
```
|
||||
|
||||
### Install packages and run
|
||||
|
||||
```bash
|
||||
pnpm i
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
### Enable plugin
|
||||
|
||||
1. open `Settings` → `Community plugins`
|
||||
2. enable the `Inline Callouts` plugin.
|
||||
|
||||
## Credits
|
||||
|
||||
Some code inspired by and derived from:
|
||||
|
||||
- [chrisgurney/obsidian-note-toolbar](https://github.com/chrisgurney/obsidian-note-toolbar/)
|
||||
- [javalent/admonitions](https://github.com/javalent/admonitions/)
|
||||
- [Moyf/easy-copy](https://github.com/Moyf/easy-copy/)
|
||||
- [nhaouari/obsidian-textgenerator-plugin](https://github.com/nhaouari/obsidian-textgenerator-plugin/)
|
||||
- [steven-kraft/obsidian-markdown-furigana](https://github.com/steven-kraft/obsidian-markdown-furigana)
|
||||
|
||||
Thank you!
|
||||
|
||||
## Notes
|
||||
|
||||
[Lucide](https://github.com/lucide-icons/lucide) Icons: https://lucide.dev
|
||||
|
||||
Lucide Icons LICENSE: https://lucide.dev/license
|
||||
48
esbuild.config.mjs
Normal file
48
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = (process.argv[2] === "production");
|
||||
|
||||
const context = await esbuild.context({
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtins],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: false,
|
||||
treeShaking: true,
|
||||
outfile: "dist/main.js",
|
||||
});
|
||||
|
||||
if (prod) {
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
} else {
|
||||
await context.watch();
|
||||
}
|
||||
10
manifest.json
Normal file
10
manifest.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"id": "inline-callouts",
|
||||
"name": "Inline Callouts",
|
||||
"version": "0.1.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Add inline callouts/badges/icons to notes.",
|
||||
"author": "@gapmiss",
|
||||
"authorUrl": "https://github.com/gapmiss",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
28
package.json
Normal file
28
package.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "inline-callouts",
|
||||
"version": "0.1.0",
|
||||
"description": "Badges for Obsidian (https://obsidian.md)",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "0.17.3",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
}
|
||||
}
|
||||
88
src/callout/builder.ts
Normal file
88
src/callout/builder.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import {
|
||||
getIconIds,
|
||||
setIcon
|
||||
} from 'obsidian';
|
||||
|
||||
export class InlineCallout {
|
||||
|
||||
newEl: HTMLSpanElement;
|
||||
iconEl: HTMLSpanElement;
|
||||
labelEl: HTMLSpanElement;
|
||||
textEl: HTMLSpanElement;
|
||||
|
||||
constructor() {
|
||||
this.newEl = document.createElement("span");
|
||||
this.iconEl = document.createElement("span");
|
||||
this.labelEl = document.createElement("span");
|
||||
this.textEl = document.createElement("span");
|
||||
}
|
||||
|
||||
public build(text: string) {
|
||||
|
||||
// text content from <code>
|
||||
let part: string = text.substring(3);
|
||||
let content: string = part.slice(0, -1);
|
||||
// parts array split on pipe
|
||||
let parts: any[] = content.split('|');
|
||||
// icon
|
||||
let calloutIcon: string | null = parts[0] ? parts[0].trim().replace(/\\+$/, '').toLowerCase() : null;
|
||||
// label
|
||||
let calloutLabel: string | null = parts[1] ? parts[1].trim().replace(/\\+$/, '') : null;
|
||||
// color
|
||||
let calloutColor: string | null = parts[2] ? parts[2].trim() : null;
|
||||
let calloutColorStyle: string;
|
||||
|
||||
// no content, return <code>
|
||||
if (
|
||||
!content.length
|
||||
|| parts.length === 0
|
||||
|| !calloutIcon
|
||||
|| (getIconIds().indexOf('lucide-' + calloutIcon) == -1 && getIconIds().indexOf(calloutIcon) == -1) // 404, no icon found
|
||||
) {
|
||||
this.newEl = document.createElement("code");
|
||||
this.newEl.setText(text)
|
||||
return this.newEl;
|
||||
}
|
||||
|
||||
this.newEl.addClass("inline-callout");
|
||||
this.newEl.setAttr("data-inline-callout", calloutIcon);
|
||||
this.iconEl.addClass("inline-callout-icon");
|
||||
this.iconEl.setAttr("data-tooltip-position", 'top');
|
||||
this.labelEl.addClass("inline-callout-label");
|
||||
|
||||
// icon only
|
||||
if (parts.length === 1) {
|
||||
this.iconEl.setAttr("aria-label", calloutIcon);
|
||||
setIcon(this.iconEl, calloutIcon);
|
||||
this.newEl.appendChild(this.iconEl);
|
||||
return this.newEl;
|
||||
}
|
||||
|
||||
// icon and color only
|
||||
if (!calloutLabel && calloutColor) {
|
||||
calloutColorStyle = "color: rgba(" + calloutColor + ", 1);"
|
||||
this.iconEl.setAttr("style", calloutColorStyle);
|
||||
this.iconEl.setAttr("aria-label", calloutIcon);
|
||||
setIcon(this.iconEl, calloutIcon);
|
||||
this.newEl.appendChild(this.iconEl);
|
||||
return this.newEl;
|
||||
}
|
||||
|
||||
// icon
|
||||
this.iconEl.setAttr("aria-label", calloutIcon);
|
||||
setIcon(this.iconEl, calloutIcon);
|
||||
this.newEl.appendChild(this.iconEl);
|
||||
// label?
|
||||
if (calloutLabel) {
|
||||
this.labelEl.setText(calloutLabel!);
|
||||
this.newEl.appendChild(this.labelEl);
|
||||
}
|
||||
// color?
|
||||
if (calloutLabel && calloutColor) {
|
||||
calloutColorStyle = "color: rgba(" + calloutColor + ", 1); background-color: rgba(" + calloutColor + ", var(--inline-callout-bg-transparency));"
|
||||
this.newEl.setAttr("style", calloutColorStyle)
|
||||
}
|
||||
return this.newEl;
|
||||
}
|
||||
|
||||
}
|
||||
262
src/main.ts
Normal file
262
src/main.ts
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
import {
|
||||
Plugin,
|
||||
MarkdownPostProcessor,
|
||||
MarkdownPostProcessorContext,
|
||||
Editor,
|
||||
MarkdownView,
|
||||
Notice,
|
||||
Menu
|
||||
} from 'obsidian';
|
||||
import {
|
||||
InlineCalloutsSettings,
|
||||
DEFAULT_SETTINGS,
|
||||
InlineCalloutsSettingTab
|
||||
} from './settings';
|
||||
import { SearchInlineCalloutsModal } from './modal/search';
|
||||
import { ModifyInlineCalloutModal } from './modal/modify';
|
||||
import { NewInlineCalloutModal } from './modal/new';
|
||||
import { EditorIconSuggest } from './suggest/editorIcon';
|
||||
import { InlineCallout } from './callout/builder';
|
||||
import { viewPlugin } from './views/editor';
|
||||
|
||||
enum ContextType {
|
||||
NULL = 'null',
|
||||
INLINECODE = 'inline-code'
|
||||
}
|
||||
|
||||
interface ContextData {
|
||||
type: ContextType;
|
||||
curLine: string;
|
||||
match: string | null;
|
||||
range: [number, number];
|
||||
}
|
||||
|
||||
export default class InlineCalloutsPlugin extends Plugin {
|
||||
|
||||
settings: InlineCalloutsSettings;
|
||||
|
||||
public postprocessor: MarkdownPostProcessor = (el: HTMLElement, ctx: MarkdownPostProcessorContext) => {
|
||||
const blockToReplace = el.querySelectorAll('code')
|
||||
if (blockToReplace.length === 0) return
|
||||
|
||||
function replace(node: Node) {
|
||||
const childrenToReplace: Text[] = []
|
||||
node.childNodes.forEach(child => {
|
||||
if (child.nodeType === 3) {
|
||||
childrenToReplace.push(child as Text)
|
||||
}
|
||||
})
|
||||
childrenToReplace.forEach((child) => {
|
||||
child.replaceWith(child);
|
||||
})
|
||||
}
|
||||
|
||||
blockToReplace.forEach(block => {
|
||||
replace(block)
|
||||
})
|
||||
}
|
||||
|
||||
async onload() {
|
||||
|
||||
await this.loadSettings();
|
||||
|
||||
this.registerMarkdownPostProcessor(
|
||||
this.buildPostProcessor()
|
||||
);
|
||||
|
||||
this.registerEditorExtension(viewPlugin)
|
||||
|
||||
if (this.settings.enableSuggester) {
|
||||
this.registerEditorSuggest(new EditorIconSuggest(this));
|
||||
}
|
||||
|
||||
this.addCommand({
|
||||
id: "new-inline-callout",
|
||||
name: "New inline callout",
|
||||
editorCallback: (editor) => {
|
||||
let modal = new NewInlineCalloutModal(this, editor);
|
||||
modal.open();
|
||||
}
|
||||
});
|
||||
|
||||
if (this.settings.enableEditing) {
|
||||
this.addCommand({
|
||||
id: "modify-inline-callout",
|
||||
name: "Modify inline callout",
|
||||
editorCheckCallback: (checking, editor, view: MarkdownView) => {
|
||||
let res = this.checkContextType(editor, view);
|
||||
if (res) {
|
||||
if (!checking) {
|
||||
this.modifyInlineCallout(editor, view);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.addCommand({
|
||||
id: "search-inline-callouts",
|
||||
name: "Search for inline callouts",
|
||||
callback: () => {
|
||||
let modal = new SearchInlineCalloutsModal(this);
|
||||
modal.open();
|
||||
}
|
||||
});
|
||||
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("editor-menu", (menu: Menu, editor: Editor, view: MarkdownView) => {
|
||||
if (this.settings.enableEditing) {
|
||||
let res = this.checkContextType(editor, view);
|
||||
if (res) {
|
||||
menu.addItem(item => {
|
||||
item
|
||||
.setTitle('Modify inline callout')
|
||||
.setIcon('quote')
|
||||
.onClick(async () => {
|
||||
this.modifyInlineCallout(editor, view);
|
||||
});
|
||||
})
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.addSettingTab(new InlineCalloutsSettingTab(this.app, this));
|
||||
|
||||
}
|
||||
|
||||
onunload() { }
|
||||
|
||||
private checkContextType(editor: Editor, view: MarkdownView) {
|
||||
const file = view.file;
|
||||
if (!file) {
|
||||
new Notice("Cannot get current file");
|
||||
return { type: ContextType.NULL, curLine: '', match: null, range: null };
|
||||
}
|
||||
|
||||
const cursor = editor.getCursor();
|
||||
const curLine = editor.getLine(cursor.line);
|
||||
const curCh = cursor.ch;
|
||||
const beforeCursor = curLine.slice(0, curCh);
|
||||
const afterCursor = curLine.slice(curCh);
|
||||
|
||||
let matcher = { type: ContextType.INLINECODE, regex: /`(\[\!\![^`]+\])`/g, enable: true }
|
||||
const matchInfo = this.getMatchInfo(beforeCursor, afterCursor, matcher.regex);
|
||||
|
||||
if (matchInfo) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private getMatchInfo(beforeCursor: string, afterCursor: string, regex: RegExp): { content: string; range: [number, number] } | null {
|
||||
let match;
|
||||
while ((match = regex.exec(beforeCursor + afterCursor)) !== null && afterCursor !== '') {
|
||||
const matchStart = match.index;
|
||||
const matchEnd = match.index + match[0].length;
|
||||
|
||||
if (beforeCursor.length >= matchStart && beforeCursor.length <= matchEnd) {
|
||||
return {
|
||||
content: match[1],
|
||||
range: [matchStart, matchEnd],
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private determineContextType(editor: Editor, view: MarkdownView): ContextData {
|
||||
const file = view.file;
|
||||
if (!file) {
|
||||
new Notice("Cannot get current file");
|
||||
return { type: ContextType.NULL, curLine: '', match: null, range: [0, 0] };
|
||||
}
|
||||
|
||||
const cursor = editor.getCursor();
|
||||
const curLine = editor.getLine(cursor.line);
|
||||
const curCh = cursor.ch;
|
||||
const beforeCursor = curLine.slice(0, curCh);
|
||||
const afterCursor = curLine.slice(curCh);
|
||||
|
||||
let matcher = { type: ContextType.INLINECODE, regex: /`(\[\!\![^`]+\])`/g, enable: true }
|
||||
const matchInfo = this.getMatchInfo(beforeCursor, afterCursor, matcher.regex);
|
||||
|
||||
if (matchInfo) {
|
||||
return {
|
||||
type: matcher.type,
|
||||
curLine,
|
||||
match: matchInfo.content,
|
||||
range: matchInfo.range,
|
||||
};
|
||||
}
|
||||
return { type: ContextType.NULL, curLine, match: null, range: [0, 0] };
|
||||
}
|
||||
|
||||
private modifyInlineCallout(editor: Editor, view: MarkdownView): void {
|
||||
|
||||
const file = view.file;
|
||||
if (!file) {
|
||||
new Notice("Cannot get current file");
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = file.basename;
|
||||
const contextType = this.determineContextType(editor, view);
|
||||
|
||||
if (contextType.type == ContextType.NULL) {
|
||||
new Notice("No inline callout found at current cursor position");
|
||||
return;
|
||||
}
|
||||
|
||||
const cursor = editor.getCursor();
|
||||
const curLine = cursor.line;
|
||||
|
||||
if (contextType.type == ContextType.INLINECODE) {
|
||||
let modal = new ModifyInlineCalloutModal(this, editor, contextType);
|
||||
modal.open();
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
buildPostProcessor(): MarkdownPostProcessor {
|
||||
return (el) => {
|
||||
el.findAll("code").forEach(
|
||||
(code) => {
|
||||
let text: string | undefined = code.innerText.trim();
|
||||
if (text !== undefined && text.startsWith('[!!') && text.endsWith(']')) {
|
||||
const inlineCallout = new InlineCallout();
|
||||
let newEl = inlineCallout.build(text);
|
||||
if (newEl !== undefined) {
|
||||
code.replaceWith(newEl);
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
this.reload();
|
||||
}
|
||||
|
||||
/** Reloads the plugin */
|
||||
async reload() {
|
||||
// @ts-ignore
|
||||
await this.app.plugins.disablePlugin("inline-callouts");
|
||||
// @ts-ignore
|
||||
await this.app.plugins.enablePlugin("inline-callouts");
|
||||
// @ts-ignore
|
||||
this.app.setting.openTabById("inline-callouts").display();
|
||||
}
|
||||
|
||||
}
|
||||
239
src/modal/modify.ts
Normal file
239
src/modal/modify.ts
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
import {
|
||||
Modal,
|
||||
Notice,
|
||||
Setting,
|
||||
Editor
|
||||
} from 'obsidian';
|
||||
import type InlineCalloutsPlugin from "../main";
|
||||
import { IconSuggest } from '../suggest/icon';
|
||||
import { InlineCallout } from '../callout/builder';
|
||||
|
||||
export class ModifyInlineCalloutModal extends Modal {
|
||||
|
||||
public calloutIcon: string;
|
||||
public calloutColor: string | undefined;
|
||||
public calloutLabel: string | undefined;
|
||||
previewEl: HTMLDivElement;
|
||||
|
||||
constructor(
|
||||
private plugin: InlineCalloutsPlugin,
|
||||
private editor: Editor,
|
||||
private contextType: any
|
||||
) {
|
||||
super(plugin.app);
|
||||
|
||||
this.containerEl.addClass("modify-inline-callout-modal");
|
||||
|
||||
this.onOpen = () => this.display(true);
|
||||
}
|
||||
|
||||
private async display(focus?: boolean, clearColor?: boolean) {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
this.titleEl.setText("Modify inline callout");
|
||||
|
||||
let content = this.contextType.match.replace("[!!", "").replace("]", "");
|
||||
let parts: any[] = content.split('|');
|
||||
|
||||
// icon
|
||||
if (clearColor) {
|
||||
// this.calloutIcon = IconSuggest.icon
|
||||
}
|
||||
else {
|
||||
this.calloutIcon = parts[0] ? parts[0].trim().replace(/\\+$/, '').toLowerCase() : 'info';
|
||||
}
|
||||
|
||||
// label
|
||||
if (!clearColor) {
|
||||
this.calloutLabel = this.calloutLabel! ? this.calloutLabel : (parts[1] ? parts[1].trim().replace(/\\+$/, '') : undefined);
|
||||
}
|
||||
|
||||
// color
|
||||
if (clearColor) {
|
||||
this.calloutColor = '';
|
||||
} else {
|
||||
this.calloutColor = this.calloutColor! ? this.calloutColor : (parts[2] ? parts[2].trim() : undefined);
|
||||
}
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Icon')
|
||||
.setDesc('To select an icon, click the button. Default: info')
|
||||
.addButton((cb) => {
|
||||
cb
|
||||
.setIcon(this.calloutIcon ? this.calloutIcon : "lucide-info")
|
||||
.setTooltip("Select icon")
|
||||
.onClick(async (e) => {
|
||||
e.preventDefault();
|
||||
const modal = new IconSuggest(this.plugin, async (icon) => {
|
||||
this.calloutIcon = icon;
|
||||
this.buildPreview();
|
||||
});
|
||||
modal.open();
|
||||
});
|
||||
cb.buttonEl.setAttribute("data-note-toolbar-no-icon", "false");
|
||||
this.plugin.registerDomEvent(
|
||||
cb.buttonEl, 'keydown', (e) => {
|
||||
switch (e.key) {
|
||||
case "Enter":
|
||||
case " ":
|
||||
e.preventDefault();
|
||||
const modal = new IconSuggest(this.plugin, async (icon) => {
|
||||
this.calloutIcon = icon;
|
||||
this.buildPreview();
|
||||
});
|
||||
modal.open();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName("Label")
|
||||
.setDesc("Default: blank")
|
||||
.addText((t) => {
|
||||
t.setValue(this.calloutLabel ? this.calloutLabel : '');
|
||||
t.onChange((v) => {
|
||||
if (v === '') {
|
||||
this.calloutLabel = undefined;
|
||||
} else {
|
||||
this.calloutLabel = v;
|
||||
}
|
||||
this.buildPreview();
|
||||
});
|
||||
});
|
||||
|
||||
const hexToRgb = (hex: string) => {
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
return `${r}, ${g}, ${b}`;
|
||||
};
|
||||
|
||||
function rgbToHex(r: any, g: any, b: any) {
|
||||
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b)
|
||||
.toString(16)
|
||||
.slice(1)
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Color')
|
||||
.setDesc('Pick a color using the dropdown or color picker. Default: Obsidian\'s base color')
|
||||
.setClass('inline-callouts-color-dropdown')
|
||||
.addDropdown((cb) => {
|
||||
cb.addOptions({
|
||||
"": "",
|
||||
"var(--color-blue-rgb)": "Blue",
|
||||
"var(--color-cyan-rgb)": "Cyan",
|
||||
"var(--color-green-rgb)": "Green",
|
||||
"var(--color-orange-rgb)": "Orange",
|
||||
"var(--color-pink-rgb)": "Pink",
|
||||
"var(--color-purple-rgb)": "Purple",
|
||||
"var(--color-red-rgb)": "Red",
|
||||
"var(--color-yellow-rgb)": "Yellow",
|
||||
})
|
||||
// .setValue(this.calloutColor!)
|
||||
.setValue(this.calloutColor?.startsWith("var(--color") ? this.calloutColor : '')
|
||||
.onChange((value) => {
|
||||
if (value !== '') {
|
||||
this.calloutColor = value;
|
||||
this.buildPreview();
|
||||
this.display(false, false);
|
||||
setTimeout(() => {
|
||||
let dropdown: HTMLSelectElement | null = window.document.querySelector(".inline-callouts-color-dropdown .dropdown");
|
||||
dropdown!.focus();
|
||||
}, 25);
|
||||
}
|
||||
})
|
||||
})
|
||||
.addColorPicker((cb) => {
|
||||
let r: number = 0;
|
||||
let g: number = 0;
|
||||
let b: number = 0;
|
||||
if (this.calloutColor) {
|
||||
let colorArr = this.calloutColor?.split(",");
|
||||
r = Number(colorArr![0]);
|
||||
g = Number(colorArr![1]);
|
||||
b = Number(colorArr![2]);
|
||||
}
|
||||
cb.setValue(rgbToHex(r, g, b))
|
||||
.onChange((value) => {
|
||||
// this.calloutColor = hexToRgb(value);
|
||||
// this.buildPreview();
|
||||
|
||||
this.calloutColor = hexToRgb(value);
|
||||
let dropdown: HTMLSelectElement | null = window.document.querySelector(".inline-callouts-color-dropdown .dropdown");
|
||||
dropdown!.value = '';
|
||||
this.buildPreview();
|
||||
setTimeout(() => {
|
||||
let picker: HTMLSelectElement | null = window.document.querySelector('input[type="color"]');
|
||||
picker!.focus();
|
||||
}, 10);
|
||||
|
||||
});
|
||||
})
|
||||
.addExtraButton((ex) => {
|
||||
ex.setIcon('refresh-ccw')
|
||||
.setTooltip('Reset to no color')
|
||||
.onClick(() => {
|
||||
this.calloutColor = '';
|
||||
this.display(false, true);
|
||||
})
|
||||
});
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Preview');
|
||||
|
||||
this.previewEl = this.contentEl.createDiv({
|
||||
cls: "inline-callouts-modal-preview"
|
||||
});
|
||||
|
||||
this.buildPreview();
|
||||
|
||||
new Setting(contentEl)
|
||||
.addButton((b) =>
|
||||
b.setButtonText("Save")
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
try {
|
||||
let firstPipe: string = '';
|
||||
let secondPipe: string = '';
|
||||
|
||||
if (this.calloutLabel !== undefined || this.calloutColor !== undefined) {
|
||||
firstPipe = "|";
|
||||
}
|
||||
|
||||
if (
|
||||
(this.calloutLabel === undefined && this.calloutColor !== undefined) ||
|
||||
(this.calloutLabel !== undefined && this.calloutColor !== undefined)
|
||||
) {
|
||||
secondPipe = "|";
|
||||
}
|
||||
|
||||
let cursor = this.editor.getCursor();
|
||||
this.editor.replaceRange(
|
||||
`\`[!!${this.calloutIcon.replace("lucide-", "")}${firstPipe + (this.calloutLabel !== undefined ? this.calloutLabel : "")}${this.calloutColor ? secondPipe + this.calloutColor : ""}]\``,
|
||||
{ line: cursor.line, ch: this.contextType.range?.[0] }, { line: cursor.line, ch: this.contextType.range?.[1] }
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
new Notice(
|
||||
"There was an issue saving the inline callout. Check the developer console for details."
|
||||
);
|
||||
}
|
||||
this.close();
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
buildPreview() {
|
||||
this.previewEl.empty();
|
||||
this.previewEl.setAttr("style", "margin-bottom: 1em;");
|
||||
const inlineCallout = new InlineCallout();
|
||||
let newEl = inlineCallout.build('[!!' + this.calloutIcon + '|' + (this.calloutLabel ? this.calloutLabel : '') + '|' + this.calloutColor + ']');
|
||||
this.previewEl.appendChild(newEl);
|
||||
}
|
||||
|
||||
}
|
||||
194
src/modal/new.ts
Normal file
194
src/modal/new.ts
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
import {
|
||||
Modal,
|
||||
Notice,
|
||||
Setting,
|
||||
Editor
|
||||
} from 'obsidian';
|
||||
import type InlineCalloutsPlugin from "../main";
|
||||
import { IconSuggest } from '../suggest/icon';
|
||||
import { InlineCallout } from '../callout/builder';
|
||||
|
||||
export class NewInlineCalloutModal extends Modal {
|
||||
|
||||
public calloutIcon: string = 'info';
|
||||
public calloutColor: string | undefined;
|
||||
public calloutLabel: string | undefined;
|
||||
previewEl: HTMLDivElement;
|
||||
|
||||
constructor(
|
||||
private plugin: InlineCalloutsPlugin,
|
||||
private editor: Editor
|
||||
) {
|
||||
super(plugin.app);
|
||||
|
||||
this.containerEl.addClass("new-inline-callout-modal");
|
||||
|
||||
this.onOpen = () => {
|
||||
if (this.editor.getSelection()) {
|
||||
this.calloutLabel = this.editor.getSelection();
|
||||
}
|
||||
this.display(true);
|
||||
}
|
||||
}
|
||||
|
||||
private async display(focus?: boolean, clearColor?: boolean) {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
this.titleEl.setText("New inline callout");
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Icon')
|
||||
.setDesc('To select an icon, click the button. Default: info')
|
||||
.addButton((cb) => {
|
||||
cb
|
||||
.setIcon('lucide-info')
|
||||
.setTooltip("Select icon")
|
||||
.onClick(async (e) => {
|
||||
e.preventDefault();
|
||||
const modal = new IconSuggest(this.plugin, async (icon) => {
|
||||
this.calloutIcon = icon;
|
||||
this.buildPreview();
|
||||
});
|
||||
modal.open();
|
||||
});
|
||||
cb.buttonEl.setAttribute("data-note-toolbar-no-icon", "false");
|
||||
this.plugin.registerDomEvent(
|
||||
cb.buttonEl, 'keydown', (e) => {
|
||||
switch (e.key) {
|
||||
case "Enter":
|
||||
case " ":
|
||||
e.preventDefault();
|
||||
const modal = new IconSuggest(this.plugin, async (icon) => {
|
||||
this.calloutIcon = icon;
|
||||
this.buildPreview();
|
||||
});
|
||||
modal.open();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName("Label")
|
||||
.setDesc("Default: blank")
|
||||
.addText((t) => {
|
||||
t.setValue(this.calloutLabel!);
|
||||
t.onChange((v) => {
|
||||
this.calloutLabel = v;
|
||||
this.buildPreview();
|
||||
});
|
||||
});
|
||||
|
||||
const hexToRgb = (hex: string) => {
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
return `${r}, ${g}, ${b}`;
|
||||
};
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Color')
|
||||
.setDesc('Pick a color using the dropdown or color picker. Default: Obsidian\'s base color')
|
||||
.setClass('inline-callouts-color-dropdown')
|
||||
.addDropdown((cb) => {
|
||||
cb.addOptions({
|
||||
"": "",
|
||||
"var(--color-blue-rgb)": "Blue",
|
||||
"var(--color-cyan-rgb)": "Cyan",
|
||||
"var(--color-green-rgb)": "Green",
|
||||
"var(--color-orange-rgb)": "Orange",
|
||||
"var(--color-pink-rgb)": "Pink",
|
||||
"var(--color-purple-rgb)": "Purple",
|
||||
"var(--color-red-rgb)": "Red",
|
||||
"var(--color-yellow-rgb)": "Yellow",
|
||||
})
|
||||
// .setValue(this.calloutColor!)
|
||||
.setValue(this.calloutColor?.startsWith("var(--color") ? this.calloutColor : '')
|
||||
.onChange((value) => {
|
||||
if (value !== '') {
|
||||
this.calloutColor = value;
|
||||
this.buildPreview();
|
||||
this.display(false, false);
|
||||
setTimeout(() => {
|
||||
let dropdown: HTMLSelectElement | null = window.document.querySelector(".inline-callouts-color-dropdown .dropdown");
|
||||
dropdown!.focus();
|
||||
}, 25);
|
||||
}
|
||||
})
|
||||
})
|
||||
.addColorPicker((cb) => {
|
||||
cb.setValue(this.calloutColor ?? '#000000')
|
||||
.onChange((value) => {
|
||||
this.calloutColor = hexToRgb(value);
|
||||
let dropdown: HTMLSelectElement | null = window.document.querySelector(".inline-callouts-color-dropdown .dropdown");
|
||||
dropdown!.value = '';
|
||||
this.buildPreview();
|
||||
setTimeout(() => {
|
||||
let picker: HTMLSelectElement | null = window.document.querySelector('input[type="color"]');
|
||||
picker!.focus();
|
||||
}, 10);
|
||||
});
|
||||
})
|
||||
.addExtraButton((ex) => {
|
||||
ex.setIcon('refresh-ccw')
|
||||
.setTooltip('Reset to no color')
|
||||
.onClick(() => {
|
||||
this.calloutColor = '';
|
||||
this.display(false, true);
|
||||
})
|
||||
});
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Preview');
|
||||
|
||||
this.previewEl = this.contentEl.createDiv({
|
||||
cls: "inline-callouts-modal-preview"
|
||||
});
|
||||
|
||||
this.buildPreview();
|
||||
|
||||
new Setting(contentEl)
|
||||
.addButton((b) =>
|
||||
b.setButtonText("Insert")
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
try {
|
||||
let firstPipe: string = '';
|
||||
let secondPipe: string = '';
|
||||
if (this.calloutLabel !== undefined || this.calloutColor !== undefined) {
|
||||
firstPipe = "|";
|
||||
}
|
||||
|
||||
if (
|
||||
(this.calloutLabel === undefined && this.calloutColor !== undefined) ||
|
||||
(this.calloutLabel !== undefined && this.calloutColor !== undefined)
|
||||
) {
|
||||
secondPipe = "|";
|
||||
}
|
||||
let trailingSpace: string = this.plugin.settings.enableTraiingSpace ? " " : "";
|
||||
this.editor.getDoc().replaceSelection(
|
||||
`\`[!!${this.calloutIcon.replace("lucide-", "")}${firstPipe + (this.calloutLabel !== undefined ? this.calloutLabel : "")}${this.calloutColor ? secondPipe + this.calloutColor : ""}]\`${trailingSpace}`,
|
||||
);
|
||||
// const cursor = this.editor.getCursor();
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
new Notice(
|
||||
"There was an issue inserting the inline callout. Please check the developer console for details."
|
||||
);
|
||||
}
|
||||
IconSuggest.icon = 'info';
|
||||
this.close();
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
buildPreview() {
|
||||
this.previewEl.empty();
|
||||
this.previewEl.setAttr("style", "margin-bottom: 1em;");
|
||||
const inlineCallout = new InlineCallout();
|
||||
let newEl = inlineCallout.build('[!!' + this.calloutIcon + '|' + (this.calloutLabel ? this.calloutLabel : '') + '|' + this.calloutColor + ']');
|
||||
this.previewEl.appendChild(newEl);
|
||||
}
|
||||
|
||||
}
|
||||
74
src/modal/search.ts
Normal file
74
src/modal/search.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import {
|
||||
Modal,
|
||||
Notice,
|
||||
Setting,
|
||||
TextComponent,
|
||||
getIconIds
|
||||
} from 'obsidian';
|
||||
import { InputIconSuggest } from '../suggest/inputIcon';
|
||||
import type InlineCalloutsPlugin from "../main";
|
||||
|
||||
export class SearchInlineCalloutsModal extends Modal {
|
||||
|
||||
public calloutIcon: string;
|
||||
|
||||
constructor(
|
||||
private plugin: InlineCalloutsPlugin,
|
||||
) {
|
||||
super(plugin.app);
|
||||
|
||||
this.containerEl.addClass("search-inline-callouts-modal");
|
||||
|
||||
this.onOpen = () => this.display(true);
|
||||
}
|
||||
|
||||
private async display(focus?: boolean) {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
let input: TextComponent;
|
||||
this.titleEl.setText("Search inline callouts");
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Icon name')
|
||||
.setDesc('Enter text to find an icon to search for.')
|
||||
.setClass('search-icon-input')
|
||||
.addSearch((t) => {
|
||||
input = t;
|
||||
t.setValue('')
|
||||
.onChange((v) => {
|
||||
this.calloutIcon = v;
|
||||
});
|
||||
new InputIconSuggest(
|
||||
this.app,
|
||||
t.inputEl,
|
||||
getIconIds().filter((icon) =>
|
||||
icon.replace('lucide-', '').toLowerCase().includes(icon.replace('lucide-', '').toLowerCase())
|
||||
)
|
||||
);
|
||||
})
|
||||
|
||||
new Setting(contentEl)
|
||||
.addButton((b) =>
|
||||
b.setButtonText("Search")
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
try {
|
||||
if (this.calloutIcon) {
|
||||
window.open(
|
||||
"obsidian://search?vault=" + encodeURIComponent(this.app.vault.getName()) + "&query=" + encodeURIComponent('/`\\[!![a-z-]*?' + this.calloutIcon.replace('lucide-', '') + '[a-z]*?/')
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
new Notice(
|
||||
"There was an issue with your search query. Please check the developer console for details.",
|
||||
5000
|
||||
);
|
||||
}
|
||||
this.close();
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
71
src/settings/index.ts
Normal file
71
src/settings/index.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import {
|
||||
App,
|
||||
PluginSettingTab,
|
||||
Setting
|
||||
} from 'obsidian';
|
||||
import InlineCalloutsPlugin from 'src/main';
|
||||
|
||||
export interface InlineCalloutsSettings {
|
||||
enableSuggester: boolean;
|
||||
enableTraiingSpace: boolean;
|
||||
enableEditing: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: InlineCalloutsSettings = {
|
||||
enableSuggester: true,
|
||||
enableTraiingSpace: true,
|
||||
enableEditing: true
|
||||
}
|
||||
|
||||
export class InlineCalloutsSettingTab extends PluginSettingTab {
|
||||
plugin: InlineCalloutsPlugin;
|
||||
|
||||
constructor(app: App, plugin: InlineCalloutsPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Enable icon suggestions')
|
||||
.setDesc('In editing view (source & live preview modes), enable inline icon auto-complete suggestions.')
|
||||
.setClass("inline-callouts-enable-icon-suggestions")
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.enableSuggester)
|
||||
.onChange(async newValue => {
|
||||
this.plugin.settings.enableSuggester = newValue;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Enable editing command/menu')
|
||||
.setDesc('In editing view (source & live preview modes), enable "Modify inline callout" command and context menu option.')
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.enableEditing)
|
||||
.onChange(async newValue => {
|
||||
this.plugin.settings.enableEditing = newValue;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
})
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Include trailing space')
|
||||
.setDesc('When inserting a new inline callout, append a trailing space immediately after the inline callout code.')
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.enableTraiingSpace)
|
||||
.onChange(async newValue => {
|
||||
this.plugin.settings.enableTraiingSpace = newValue;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
61
src/suggest/editorIcon.ts
Normal file
61
src/suggest/editorIcon.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import {
|
||||
TFile,
|
||||
getIconIds,
|
||||
Editor,
|
||||
EditorPosition,
|
||||
EditorSuggestTriggerInfo,
|
||||
EditorSuggest,
|
||||
EditorSuggestContext,
|
||||
setIcon
|
||||
} from 'obsidian';
|
||||
import InlineCalloutsPlugin from '../main';
|
||||
|
||||
export class EditorIconSuggest extends EditorSuggest<string> {
|
||||
plugin: InlineCalloutsPlugin;
|
||||
|
||||
constructor(plugin: InlineCalloutsPlugin) {
|
||||
super(plugin.app);
|
||||
this.plugin = plugin;
|
||||
this.setInstructions([
|
||||
{ command: '↑↓', purpose: 'navigate' },
|
||||
{ command: '↵', purpose: 'select' },
|
||||
{ command: 'esc', purpose: 'dismiss' },
|
||||
]);
|
||||
}
|
||||
|
||||
onTrigger(cursor: EditorPosition, editor: Editor, _: TFile): EditorSuggestTriggerInfo | null {
|
||||
const sub = editor.getLine(cursor.line).substring(0, cursor.ch);
|
||||
const match = sub.match(/!!\S+$/)?.first();
|
||||
if (match) {
|
||||
return {
|
||||
end: cursor,
|
||||
start: {
|
||||
ch: sub.lastIndexOf(match),
|
||||
line: cursor.line,
|
||||
},
|
||||
query: match,
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getSuggestions(context: EditorSuggestContext): string[] {
|
||||
let icon_query = context.query.replace('!!', '').toLowerCase();
|
||||
let iconNames = getIconIds().map(p => p.replace("lucide-", ""));
|
||||
return iconNames.filter(p => p.includes(icon_query));
|
||||
}
|
||||
|
||||
renderSuggestion(suggestion: string, el: HTMLElement): void {
|
||||
const outer = el.createDiv({ cls: "icon-suggester-container" });
|
||||
setIcon(outer, suggestion);
|
||||
outer.createSpan({ cls: "icon-suggester-name" }, cb => {
|
||||
cb.setAttr("style", "margin-left: .75em; vertical-align:top;");
|
||||
}).setText(suggestion);
|
||||
}
|
||||
|
||||
selectSuggestion(suggestion: string): void {
|
||||
if (this.context) {
|
||||
(this.context.editor as Editor).replaceRange('!!' + suggestion.replace("lucide-", ""), this.context.start, this.context.end);
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/suggest/icon.ts
Normal file
63
src/suggest/icon.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import {
|
||||
getIconIds,
|
||||
setIcon,
|
||||
IconName,
|
||||
SuggestModal
|
||||
} from 'obsidian';
|
||||
import type InlineCalloutsPlugin from "../main";
|
||||
|
||||
export class IconSuggest extends SuggestModal<IconName> {
|
||||
public plugin: InlineCalloutsPlugin;
|
||||
private callback: (icon: string) => void;
|
||||
static icon: string = 'info';
|
||||
|
||||
constructor(plugin: InlineCalloutsPlugin, callback: (icon: string) => void) {
|
||||
super(plugin.app);
|
||||
this.modalEl.addClass("vault-name-icon-select-modal");
|
||||
this.plugin = plugin;
|
||||
this.callback = callback;
|
||||
this.setPlaceholder("Search for an icon");
|
||||
this.setInstructions([
|
||||
{ command: '↑↓', purpose: 'to navigate' },
|
||||
{ command: '↵', purpose: 'to use' },
|
||||
{ command: 'esc', purpose: 'to dismiss' },
|
||||
]);
|
||||
}
|
||||
|
||||
getSuggestions(inputStr: string): IconName[] {
|
||||
const iconIds = getIconIds();
|
||||
const iconSuggestions: IconName[] = [];
|
||||
const lowerCaseInputStr = inputStr.toLowerCase();
|
||||
// iconSuggestions.push("No icon");
|
||||
iconIds.forEach((icon: IconName) => {
|
||||
if (icon.toLowerCase().contains(lowerCaseInputStr)) {
|
||||
iconSuggestions.push(icon);
|
||||
}
|
||||
});
|
||||
return iconSuggestions;
|
||||
}
|
||||
|
||||
renderSuggestion(icon: IconName, el: HTMLElement): void {
|
||||
el.addClass("vault-name-icon-suggestion");
|
||||
let iconWrapper = el.createDiv();
|
||||
let iconName = el.createSpan();
|
||||
iconName.setAttr("style", "margin-left: .75em; vertical-align:top;");
|
||||
let iconGlyph = el.createSpan();
|
||||
if (icon === "No icon") {
|
||||
iconName.setText(icon);
|
||||
}
|
||||
else {
|
||||
iconName.setText((icon.startsWith("lucide-") ? icon.substring(7) : icon) + " ");
|
||||
setIcon(iconGlyph, icon);
|
||||
}
|
||||
iconWrapper.appendChild(iconGlyph);
|
||||
iconWrapper.appendChild(iconName);
|
||||
}
|
||||
|
||||
onChooseSuggestion(item: string, evt: MouseEvent | KeyboardEvent) {
|
||||
setIcon(activeDocument.querySelector('[data-note-toolbar-no-icon]')!, item);
|
||||
IconSuggest.icon = item;
|
||||
this.callback(item);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
42
src/suggest/inputIcon.ts
Normal file
42
src/suggest/inputIcon.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import {
|
||||
AbstractInputSuggest,
|
||||
App,
|
||||
setIcon
|
||||
} from "obsidian";
|
||||
|
||||
export class InputIconSuggest extends AbstractInputSuggest<String> {
|
||||
textInputEl: HTMLInputElement;
|
||||
|
||||
constructor(
|
||||
public app: App,
|
||||
public inputEl: HTMLInputElement,
|
||||
private items: string[],
|
||||
|
||||
) {
|
||||
super(app, inputEl);
|
||||
}
|
||||
|
||||
getSuggestions(inputStr: string): string[] {
|
||||
const inputLowerCase: string = inputStr.toLowerCase();
|
||||
const filtered = this.items.filter((item) => {
|
||||
if (item.replace('lucide-', '').toLowerCase().contains(inputLowerCase)) return item;
|
||||
});
|
||||
if (!filtered) this.close();
|
||||
return filtered;
|
||||
}
|
||||
|
||||
renderSuggestion(value: string, el: HTMLElement): void {
|
||||
let span: HTMLSpanElement = el.createSpan();
|
||||
setIcon(span, value);
|
||||
span.setAttr("style", "display: inline-block; margin-right: .5em;");
|
||||
let label: HTMLSpanElement = el.createSpan({ text: " " + value.replace('lucide-', '') });
|
||||
label.setAttr("style", "display: inline-block; vertical-align: top;");
|
||||
}
|
||||
|
||||
selectSuggestion(item: string): void {
|
||||
this.textInputEl.value = item.replace('lucide-', '');
|
||||
this.textInputEl.trigger("input");
|
||||
this.close();
|
||||
}
|
||||
|
||||
}
|
||||
86
src/views/editor.ts
Normal file
86
src/views/editor.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import {
|
||||
editorLivePreviewField
|
||||
} from 'obsidian';
|
||||
import { ViewPlugin, WidgetType, EditorView, ViewUpdate, Decoration, DecorationSet } from '@codemirror/view';
|
||||
import { RangeSetBuilder } from "@codemirror/state";
|
||||
import { InlineCallout } from '../callout/builder';
|
||||
|
||||
const REGEXP = /(`\[!!([^\]]*)\]`)/gm;
|
||||
|
||||
export const viewPlugin = ViewPlugin.fromClass(class {
|
||||
decorations: DecorationSet;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = this.buildDecorations(view);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
this.decorations = this.buildDecorations(update.view);
|
||||
}
|
||||
|
||||
destroy() { }
|
||||
|
||||
buildDecorations(view: EditorView): DecorationSet {
|
||||
if (!view.state.field(editorLivePreviewField)) {
|
||||
return Decoration.none;
|
||||
}
|
||||
let builder = new RangeSetBuilder<Decoration>();
|
||||
let lines: number[] = [];
|
||||
if (view.state.doc.length > 0) {
|
||||
lines = Array.from(
|
||||
{ length: view.state.doc.lines },
|
||||
(_, i) => i + 1,
|
||||
);
|
||||
}
|
||||
|
||||
const currentSelections = [...view.state.selection.ranges];
|
||||
|
||||
for (let n of lines) {
|
||||
const line = view.state.doc.line(n);
|
||||
const startOfLine = line.from;
|
||||
const endOfLine = line.to;
|
||||
|
||||
let currentLine = false;
|
||||
currentSelections.forEach((r) => {
|
||||
if (r.to >= startOfLine && r.from <= endOfLine) {
|
||||
currentLine = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
let matches = Array.from(line.text.matchAll(REGEXP))
|
||||
for (const match of matches) {
|
||||
let add = true;
|
||||
const from = match.index != undefined ? match.index + line.from : -1
|
||||
const to = from + match[0].length
|
||||
if ((to - from) === 6) {
|
||||
add = false
|
||||
}
|
||||
currentSelections.forEach((r) => {
|
||||
if (r.to >= from && r.from <= to) {
|
||||
add = false
|
||||
}
|
||||
})
|
||||
if (add) {
|
||||
builder.add(from, to, Decoration.widget({ widget: new InlineCalloutWidget(match) }))
|
||||
}
|
||||
}
|
||||
}
|
||||
return builder.finish();
|
||||
}
|
||||
}, {
|
||||
decorations: (v: any) => v.decorations,
|
||||
})
|
||||
|
||||
class InlineCalloutWidget extends WidgetType {
|
||||
constructor(readonly callout: string[]) {
|
||||
super()
|
||||
}
|
||||
|
||||
toDOM(view: EditorView): HTMLElement {
|
||||
let text: string = this.callout[0].substring(1).substring(this.callout[0].length - 2, 0);
|
||||
const inlineCallout = new InlineCallout();
|
||||
let newEl = inlineCallout.build(text);
|
||||
return newEl;
|
||||
}
|
||||
}
|
||||
177
styles.css
Normal file
177
styles.css
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
body {
|
||||
--inline-callout-font-size: .85em;
|
||||
--inline-callout-font-weight: 400;
|
||||
--inline-callout-border-radius: 4px;
|
||||
--inline-callout-bg-transparency: .1;
|
||||
--inline-callout-margin: 1px;
|
||||
--inline-callout-padding-top: 1px;
|
||||
--inline-callout-padding-right: 0px;
|
||||
--inline-callout-padding-bottom: 2px;
|
||||
--inline-callout-padding-left: 0px;
|
||||
--inline-callout-icon-size: .85em;
|
||||
--inline-callout-icon-margin-top: 3px;
|
||||
--inline-callout-icon-margin-right: 3px;
|
||||
--inline-callout-icon-margin-bottom: 0px;
|
||||
--inline-callout-icon-margin-left: 5px;
|
||||
--inline-callout-label-margin-top: 2px;
|
||||
--inline-callout-label-margin-right: 5px;
|
||||
--inline-callout-label-margin-bottom: 0px;
|
||||
--inline-callout-label-margin-left: 2px;
|
||||
}
|
||||
|
||||
.inline-callout {
|
||||
margin: var(--inline-callout-margin);
|
||||
border-radius: var(--inline-callout-border-radius);
|
||||
font-weight: var(--inline-callout-font-weight);
|
||||
padding-top: var(--inline-callout-padding-top);
|
||||
padding-right: var(--inline-callout-padding-right);
|
||||
padding-bottom: var(--inline-callout-padding-bottom);
|
||||
padding-left: var(--inline-callout-padding-left);
|
||||
cursor: default;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.inline-callout-icon {
|
||||
margin-top: var(--inline-callout-icon-margin-top);
|
||||
margin-right: var(--inline-callout-icon-margin-right);
|
||||
margin-bottom: var(--inline-callout-icon-margin-bottom);
|
||||
margin-left: var(--inline-callout-icon-margin-left);
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.inline-callout-icon.inline-callout-icon svg {
|
||||
width: var(--inline-callout-icon-size);
|
||||
height: var(--inline-callout-icon-size);
|
||||
}
|
||||
|
||||
.inline-callout-label {
|
||||
font-size: var(--inline-callout-font-size);
|
||||
margin-top: var(--inline-callout-label-margin-top);
|
||||
margin-right: var(--inline-callout-label-margin-right);
|
||||
margin-bottom: var(--inline-callout-label-margin-bottom);
|
||||
margin-left: var(--inline-callout-label-margin-left);
|
||||
display: inline-flex;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* @settings
|
||||
name: Inline Callouts
|
||||
id: inline-callouts
|
||||
settings:
|
||||
-
|
||||
id: callout-settings
|
||||
title: Callout
|
||||
type: heading
|
||||
level: 4
|
||||
collapsed: true
|
||||
-
|
||||
id: inline-callout-border-radius
|
||||
title: Border radius
|
||||
type: variable-text
|
||||
default: 4px
|
||||
-
|
||||
id: inline-callout-bg-transparency
|
||||
title: Background transparency
|
||||
type: variable-number-slider
|
||||
default: .1
|
||||
min: .0
|
||||
max: .9
|
||||
step: .05
|
||||
-
|
||||
id: inline-callout-margin
|
||||
title: Margin
|
||||
type: variable-text
|
||||
default: 1px
|
||||
-
|
||||
id: inline-callout-padding-top
|
||||
title: Top padding
|
||||
type: variable-text
|
||||
default: 1px
|
||||
-
|
||||
id: inline-callout-padding-right
|
||||
title: Right padding
|
||||
type: variable-text
|
||||
default: 0px
|
||||
-
|
||||
id: inline-callout-padding-bottom
|
||||
title: Bottom padding
|
||||
type: variable-text
|
||||
default: 2px
|
||||
-
|
||||
id: inline-callout-padding-left
|
||||
title: Left padding
|
||||
type: variable-text
|
||||
default: 0px
|
||||
-
|
||||
id: icon-settings
|
||||
title: Icon
|
||||
type: heading
|
||||
level: 4
|
||||
collapsed: true
|
||||
-
|
||||
id: inline-callout-icon-size
|
||||
title: Size
|
||||
description: "Both width and height"
|
||||
type: variable-text
|
||||
default: .85em
|
||||
-
|
||||
id: inline-callout-icon-margin-top
|
||||
title: Top margin
|
||||
type: variable-text
|
||||
default: 3px
|
||||
-
|
||||
id: inline-callout-icon-margin-right
|
||||
title: Right margin
|
||||
type: variable-text
|
||||
default: 3px
|
||||
-
|
||||
id: inline-callout-icon-margin-bottom
|
||||
title: Bottom margin
|
||||
type: variable-text
|
||||
default: 0px
|
||||
-
|
||||
id: inline-callout-icon-margin-left
|
||||
title: Left margin
|
||||
type: variable-text
|
||||
default: 5px
|
||||
-
|
||||
id: label-settings
|
||||
title: Label
|
||||
type: heading
|
||||
level: 4
|
||||
collapsed: true
|
||||
-
|
||||
id: inline-callout-font-size
|
||||
title: Font size
|
||||
type: variable-text
|
||||
default: .85em
|
||||
-
|
||||
id: inline-callout-font-weight
|
||||
title: Font weight
|
||||
type: variable-number-slider
|
||||
default: 400
|
||||
min: 100
|
||||
max: 900
|
||||
step: 100
|
||||
-
|
||||
id: inline-callout-label-margin-top
|
||||
title: Top margin
|
||||
type: variable-text
|
||||
default: 2px
|
||||
-
|
||||
id: inline-callout-label-margin-right
|
||||
title: Right margin
|
||||
type: variable-text
|
||||
default: 5px
|
||||
-
|
||||
id: inline-callout-label-margin-bottom
|
||||
title: Bottom margin
|
||||
type: variable-text
|
||||
default: 0px
|
||||
-
|
||||
id: inline-callout-label-margin-left
|
||||
title: Left margin
|
||||
type: variable-text
|
||||
default: 2px
|
||||
-
|
||||
*/
|
||||
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES5",
|
||||
"ES6",
|
||||
"ES7"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
||||
14
version-bump.mjs
Normal file
14
version-bump.mjs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { readFileSync, writeFileSync } from "fs";
|
||||
|
||||
const targetVersion = process.env.npm_package_version;
|
||||
|
||||
// read minAppVersion from manifest.json and bump version to target version
|
||||
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
|
||||
const { minAppVersion } = manifest;
|
||||
manifest.version = targetVersion;
|
||||
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
|
||||
|
||||
// update versions.json with target version and minAppVersion from manifest.json
|
||||
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
|
||||
versions[targetVersion] = minAppVersion;
|
||||
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
|
||||
4
versions.json
Normal file
4
versions.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"1.1.0": "0.15.0",
|
||||
"1.0.0": "0.15.0"
|
||||
}
|
||||
Loading…
Reference in a new issue