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" +}