From 77f3a9876542887d264582e20c7c15a1913221b7 Mon Sep 17 00:00:00 2001
From: gapmiss <98914514+gapmiss@users.noreply.github.com>
Date: Fri, 4 Apr 2025 18:11:01 -0500
Subject: [PATCH] Initial commit
---
.editorconfig | 10 ++
.eslintignore | 3 +
.eslintrc | 23 ++++
.gitignore | 27 ++++
.npmrc | 1 +
LICENSE | 21 +++
README.md | 152 ++++++++++++++++++++++
esbuild.config.mjs | 48 +++++++
manifest.json | 10 ++
package.json | 28 ++++
src/callout/builder.ts | 88 +++++++++++++
src/main.ts | 262 ++++++++++++++++++++++++++++++++++++++
src/modal/modify.ts | 239 ++++++++++++++++++++++++++++++++++
src/modal/new.ts | 194 ++++++++++++++++++++++++++++
src/modal/search.ts | 74 +++++++++++
src/settings/index.ts | 71 +++++++++++
src/suggest/editorIcon.ts | 61 +++++++++
src/suggest/icon.ts | 63 +++++++++
src/suggest/inputIcon.ts | 42 ++++++
src/views/editor.ts | 86 +++++++++++++
styles.css | 177 +++++++++++++++++++++++++
tsconfig.json | 25 ++++
version-bump.mjs | 14 ++
versions.json | 4 +
24 files changed, 1723 insertions(+)
create mode 100644 .editorconfig
create mode 100644 .eslintignore
create mode 100644 .eslintrc
create mode 100644 .gitignore
create mode 100644 .npmrc
create mode 100644 LICENSE
create mode 100644 README.md
create mode 100644 esbuild.config.mjs
create mode 100644 manifest.json
create mode 100644 package.json
create mode 100644 src/callout/builder.ts
create mode 100644 src/main.ts
create mode 100644 src/modal/modify.ts
create mode 100644 src/modal/new.ts
create mode 100644 src/modal/search.ts
create mode 100644 src/settings/index.ts
create mode 100644 src/suggest/editorIcon.ts
create mode 100644 src/suggest/icon.ts
create mode 100644 src/suggest/inputIcon.ts
create mode 100644 src/views/editor.ts
create mode 100644 styles.css
create mode 100644 tsconfig.json
create mode 100644 version-bump.mjs
create mode 100644 versions.json
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..84b8a66
--- /dev/null
+++ b/.editorconfig
@@ -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
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..e019f3c
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,3 @@
+node_modules/
+
+main.js
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..0807290
--- /dev/null
+++ b/.eslintrc
@@ -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"
+ }
+ }
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ed98f50
--- /dev/null
+++ b/.gitignore
@@ -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
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..b973752
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+tag-version-prefix=""
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..e0b0b3d
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9e78906
--- /dev/null
+++ b/README.md
@@ -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 ` backtick symbols, as they are used as delimiters for the custom syntax.
+
+## Caveats
+
+- No extraneous `|` or ` 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
diff --git a/esbuild.config.mjs b/esbuild.config.mjs
new file mode 100644
index 0000000..fc66070
--- /dev/null
+++ b/esbuild.config.mjs
@@ -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();
+}
\ No newline at end of file
diff --git a/manifest.json b/manifest.json
new file mode 100644
index 0000000..4eba85f
--- /dev/null
+++ b/manifest.json
@@ -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
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..9aa02f8
--- /dev/null
+++ b/package.json
@@ -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"
+ }
+}
diff --git a/src/callout/builder.ts b/src/callout/builder.ts
new file mode 100644
index 0000000..b8f3e1c
--- /dev/null
+++ b/src/callout/builder.ts
@@ -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
+ 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
+ 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;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..1dcfafb
--- /dev/null
+++ b/src/main.ts
@@ -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();
+ }
+
+}
diff --git a/src/modal/modify.ts b/src/modal/modify.ts
new file mode 100644
index 0000000..0a59cb3
--- /dev/null
+++ b/src/modal/modify.ts
@@ -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);
+ }
+
+}
diff --git a/src/modal/new.ts b/src/modal/new.ts
new file mode 100644
index 0000000..ed11da4
--- /dev/null
+++ b/src/modal/new.ts
@@ -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);
+ }
+
+}
diff --git a/src/modal/search.ts b/src/modal/search.ts
new file mode 100644
index 0000000..cff9358
--- /dev/null
+++ b/src/modal/search.ts
@@ -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();
+ })
+ );
+
+ }
+
+}
diff --git a/src/settings/index.ts b/src/settings/index.ts
new file mode 100644
index 0000000..e758f1e
--- /dev/null
+++ b/src/settings/index.ts
@@ -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();
+ })
+ })
+
+ }
+}
diff --git a/src/suggest/editorIcon.ts b/src/suggest/editorIcon.ts
new file mode 100644
index 0000000..fd4d09e
--- /dev/null
+++ b/src/suggest/editorIcon.ts
@@ -0,0 +1,61 @@
+import {
+ TFile,
+ getIconIds,
+ Editor,
+ EditorPosition,
+ EditorSuggestTriggerInfo,
+ EditorSuggest,
+ EditorSuggestContext,
+ setIcon
+} from 'obsidian';
+import InlineCalloutsPlugin from '../main';
+
+export class EditorIconSuggest extends EditorSuggest {
+ 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/suggest/icon.ts b/src/suggest/icon.ts
new file mode 100644
index 0000000..4e19ff7
--- /dev/null
+++ b/src/suggest/icon.ts
@@ -0,0 +1,63 @@
+import {
+ getIconIds,
+ setIcon,
+ IconName,
+ SuggestModal
+} from 'obsidian';
+import type InlineCalloutsPlugin from "../main";
+
+export class IconSuggest extends SuggestModal {
+ 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();
+ }
+}
diff --git a/src/suggest/inputIcon.ts b/src/suggest/inputIcon.ts
new file mode 100644
index 0000000..8ed5a82
--- /dev/null
+++ b/src/suggest/inputIcon.ts
@@ -0,0 +1,42 @@
+import {
+ AbstractInputSuggest,
+ App,
+ setIcon
+} from "obsidian";
+
+export class InputIconSuggest extends AbstractInputSuggest {
+ 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();
+ }
+
+}
\ No newline at end of file
diff --git a/src/views/editor.ts b/src/views/editor.ts
new file mode 100644
index 0000000..2cbe4af
--- /dev/null
+++ b/src/views/editor.ts
@@ -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();
+ 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;
+ }
+}
diff --git a/styles.css b/styles.css
new file mode 100644
index 0000000..52843cc
--- /dev/null
+++ b/styles.css
@@ -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
+-
+*/
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..580b8e2
--- /dev/null
+++ b/tsconfig.json
@@ -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"
+ ]
+}
diff --git a/version-bump.mjs b/version-bump.mjs
new file mode 100644
index 0000000..d409fa0
--- /dev/null
+++ b/version-bump.mjs
@@ -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"));
diff --git a/versions.json b/versions.json
new file mode 100644
index 0000000..d39b855
--- /dev/null
+++ b/versions.json
@@ -0,0 +1,4 @@
+{
+ "1.1.0": "0.15.0",
+ "1.0.0": "0.15.0"
+}