commit 77f3a9876542887d264582e20c7c15a1913221b7
Author: gapmiss <98914514+gapmiss@users.noreply.github.com>
Date: Fri Apr 4 18:11:01 2025 -0500
Initial commit
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"
+}