mirror of
https://github.com/jsmorabito/obsidian-text-formatting-toolbar.git
synced 2026-07-22 07:48:21 +00:00
feat: restore wikilink toggle button and declarative Settings API, bump to 0.2.4
Re-applies the changes held back on wikilink-and-settings-api after confirming the "attestation failed" review result was caused by an upstream bug in Obsidian's community review backend (it doesn't yet handle a GitHub REST API breaking change to attestation responses), not by anything in this repo or release. Combines that restored code with the manifest.json attestation fix already on main.
This commit is contained in:
parent
d1ffaf19f4
commit
2854493476
10 changed files with 207 additions and 46 deletions
|
|
@ -1,6 +1,6 @@
|
|||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/b278d61d-7ab0-431f-b911-2474d16381a7">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/996ae572-1f24-4b22-b972-b67215cb8fd2">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/94439f21-bf96-43fa-88e9-217bd96b11c8">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/f747ca4d-12be-4912-bb7a-b390f8806a71">
|
||||
<img alt="Shows project promo image in light and dark mode">
|
||||
</picture>
|
||||
|
||||
|
|
@ -15,6 +15,7 @@ When you select text in a note, a toolbar appears with one-click access to commo
|
|||
- **Text Style** — set heading level (H1–H6) or paragraph
|
||||
- **Bold**, **Italic**, **Strikethrough**, **Highlight**
|
||||
- **Link** — wrap selection in a Markdown link
|
||||
- **Wikilink** - wrap selection in brackets
|
||||
- **Inline Code** / **Code Block**
|
||||
- **Comment** — wrap in Obsidian comment syntax (`%% ... %%`)
|
||||
- **Lists** — bullet list, numbered list, or checkbox
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"id": "text-formatting-toolbar",
|
||||
"name": "Text Formatting Toolbar",
|
||||
"version": "0.2.3",
|
||||
"minAppVersion": "1.0.0",
|
||||
"version": "0.2.4",
|
||||
"minAppVersion": "1.13.0",
|
||||
"description": "A floating formatting toolbar that appears above selected text in the editor.",
|
||||
"author": "John Morabito",
|
||||
"authorUrl": "https://github.com/jsmorabito",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "text-toolbar",
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.4",
|
||||
"description": "A floating formatting toolbar for Obsidian",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
applyHeading,
|
||||
toggleBlockquote,
|
||||
toggleCodeBlock,
|
||||
toggleWikilink,
|
||||
wrapLink,
|
||||
type SelectionInfo,
|
||||
} from "../formatting";
|
||||
|
|
@ -282,3 +283,60 @@ describe("wrapLink", () => {
|
|||
expect(ch).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// toggleWikilink
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("toggleWikilink — wrap", () => {
|
||||
it("wraps plain text in double brackets", () => {
|
||||
const r = toggleWikilink(sel("my note"));
|
||||
expect(r.replacement).toBe("[[my note]]");
|
||||
expect(r.newFrom).toEqual({ line: 0, ch: 2 });
|
||||
expect(r.newTo).toEqual({ line: 0, ch: 9 });
|
||||
});
|
||||
|
||||
it("wraps empty selection", () => {
|
||||
const r = toggleWikilink(sel(""));
|
||||
expect(r.replacement).toBe("[[]]");
|
||||
expect(r.newFrom).toEqual({ line: 0, ch: 2 });
|
||||
expect(r.newTo).toEqual({ line: 0, ch: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("toggleWikilink — unwrap selection-wrapped", () => {
|
||||
it("unwraps [[note]] when selection contains the brackets", () => {
|
||||
const r = toggleWikilink(sel("[[my note]]"));
|
||||
expect(r.replacement).toBe("my note");
|
||||
expect(r.newFrom).toEqual({ line: 0, ch: 2 });
|
||||
expect(r.newTo).toEqual({ line: 0, ch: 9 });
|
||||
});
|
||||
|
||||
it("does NOT unwrap when content is just the brackets (too short)", () => {
|
||||
// "[[]]" length is 4, not > 2+2
|
||||
const r = toggleWikilink(sel("[[]]"));
|
||||
expect(r.replacement).toBe("[[[[]]]]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("toggleWikilink — unwrap outside-selection", () => {
|
||||
it("unwraps when brackets sit just outside the selection", () => {
|
||||
// full line: [[my note]]
|
||||
// selection: my note (ch 2..9)
|
||||
const info: SelectionInfo = {
|
||||
sel: "my note",
|
||||
from: { line: 0, ch: 2 },
|
||||
to: { line: 0, ch: 9 },
|
||||
line: "[[my note]]",
|
||||
};
|
||||
const r = toggleWikilink(info);
|
||||
expect(r.replacement).toBe("my note");
|
||||
expect(r.newFrom).toEqual({ line: 0, ch: 0 });
|
||||
expect(r.newTo).toEqual({ line: 0, ch: 7 });
|
||||
});
|
||||
|
||||
it("wraps normally when brackets are NOT outside the selection", () => {
|
||||
const r = toggleWikilink(sel("my note"));
|
||||
expect(r.replacement).toBe("[[my note]]");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { WIKILINK_ICON_ID } from "./icons";
|
||||
import { TextToolbarSettings } from "./types";
|
||||
|
||||
export const TEXT_TOOLBAR_DEFAULT_COMMANDS: {
|
||||
|
|
@ -12,6 +13,7 @@ export const TEXT_TOOLBAR_DEFAULT_COMMANDS: {
|
|||
{ id: "strikethrough", icon: "strikethrough", name: "Strikethrough", description: "Toggle strikethrough formatting" },
|
||||
{ id: "highlight", icon: "highlighter", name: "Highlight", description: "Toggle highlight formatting" },
|
||||
{ id: "link", icon: "link-2", name: "Link", description: "Wrap selected text in a markdown link" },
|
||||
{ id: "wikilink", icon: WIKILINK_ICON_ID,name: "Wikilink", description: "Toggle double brackets around selected text to link to a note" },
|
||||
{ id: "code", icon: "code", name: "Inline Code", description: "Toggle inline code formatting" },
|
||||
{ id: "code-block", icon: "code-2", name: "Code Block", description: "Wrap selection in a fenced code block" },
|
||||
{ id: "comment", icon: "message-square",name: "Comment", description: "Wrap selection in an Obsidian comment (%% hidden from preview %%)" },
|
||||
|
|
|
|||
|
|
@ -107,3 +107,51 @@ export function toggleCodeBlock(sel: string): string {
|
|||
export function wrapLink(sel: string, fromCh: number): [string, number] {
|
||||
return [`[${sel}]()`, fromCh + 1 + sel.length + 2];
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles a `[[double bracket]]` wikilink wrap for Obsidian note links.
|
||||
* Same wrap/unwrap shape as toggleInlineFormat, but with distinct
|
||||
* open/close markers instead of a single symmetric one.
|
||||
*/
|
||||
export function toggleWikilink(info: SelectionInfo): ToggleResult {
|
||||
const { sel, from, to, line } = info;
|
||||
const open = "[[";
|
||||
const close = "]]";
|
||||
|
||||
// Case 1: selection itself is wrapped — unwrap
|
||||
if (
|
||||
from.line === to.line &&
|
||||
sel.length > open.length + close.length &&
|
||||
sel.startsWith(open) &&
|
||||
sel.endsWith(close)
|
||||
) {
|
||||
const inner = sel.slice(open.length, sel.length - close.length);
|
||||
return {
|
||||
replacement: inner,
|
||||
newFrom: { line: from.line, ch: from.ch + open.length },
|
||||
newTo: { line: from.line, ch: from.ch + open.length + inner.length },
|
||||
};
|
||||
}
|
||||
|
||||
// Case 2: markers are just outside the selection — expand-select then unwrap
|
||||
if (from.line === to.line && from.ch >= open.length && to.ch + close.length <= line.length) {
|
||||
const before = line.substring(from.ch - open.length, from.ch);
|
||||
const after = line.substring(to.ch, to.ch + close.length);
|
||||
if (before === open && after === close) {
|
||||
return {
|
||||
replacement: sel,
|
||||
newFrom: { line: from.line, ch: from.ch - open.length },
|
||||
newTo: { line: from.line, ch: from.ch - open.length + sel.length },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Case 3: wrap
|
||||
return {
|
||||
replacement: `${open}${sel}${close}`,
|
||||
newFrom: { line: from.line, ch: from.ch + open.length },
|
||||
newTo: from.line === to.line
|
||||
? { line: from.line, ch: from.ch + open.length + sel.length }
|
||||
: to,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
14
src/icons.ts
Normal file
14
src/icons.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* Custom icons registered via Obsidian's `addIcon`, which renders content
|
||||
* inside a fixed 100x100 viewBox — so artwork authored on a 24x24 grid is
|
||||
* wrapped in a scale transform to preserve its proportions.
|
||||
*/
|
||||
|
||||
export const WIKILINK_ICON_ID = "tt-double-brackets";
|
||||
|
||||
export const WIKILINK_ICON_SVG = `<g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" transform="scale(4.16667)">
|
||||
<path d="M14 5H15.5C15.6326 5 15.7598 5.08194 15.8536 5.22781C15.9473 5.37367 16 5.5715 16 5.77778V18.2222C16 18.4285 15.9473 18.6263 15.8536 18.7722C15.7598 18.9181 15.6326 19 15.5 19H14"/>
|
||||
<path d="M19 5H20.5C20.6326 5 20.7598 5.08194 20.8536 5.22781C20.9473 5.37367 21 5.5715 21 5.77778V18.2222C21 18.4285 20.9473 18.6263 20.8536 18.7722C20.7598 18.9181 20.6326 19 20.5 19H19"/>
|
||||
<path d="M10 19H8.5C8.36739 19 8.24021 18.9181 8.14645 18.7722C8.05268 18.6263 8 18.4285 8 18.2222V5.77778C8 5.5715 8.05268 5.37367 8.14645 5.22781C8.24021 5.08194 8.36739 5 8.5 5H10"/>
|
||||
<path d="M5 19H3.5C3.36739 19 3.24021 18.9181 3.14645 18.7722C3.05268 18.6263 3 18.4285 3 18.2222V5.77778C3 5.5715 3.05268 5.37367 3.14645 5.22781C3.24021 5.08194 3.36739 5 3.5 5H5"/>
|
||||
</g>`;
|
||||
81
src/main.ts
81
src/main.ts
|
|
@ -1,5 +1,6 @@
|
|||
import { App, Plugin, PluginSettingTab, Setting } from "obsidian";
|
||||
import { addIcon, App, Plugin, PluginSettingTab, SettingDefinitionItem } from "obsidian";
|
||||
import { DEFAULT_SETTINGS, TEXT_TOOLBAR_DEFAULT_COMMANDS } from "./constants";
|
||||
import { WIKILINK_ICON_ID, WIKILINK_ICON_SVG } from "./icons";
|
||||
import { TextToolbarAPI, TextToolbarExternalCommand, TextToolbarSettings } from "./types";
|
||||
import TextToolbarManager from "./textToolbarManager";
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ export default class TextToolbarPlugin extends Plugin {
|
|||
};
|
||||
|
||||
public async onload(): Promise<void> {
|
||||
addIcon(WIKILINK_ICON_ID, WIKILINK_ICON_SVG);
|
||||
await this.loadSettings();
|
||||
this.manager = new TextToolbarManager(this);
|
||||
this.addSettingTab(new TextToolbarSettingTab(this.app, this));
|
||||
|
|
@ -47,46 +49,45 @@ class TextToolbarSettingTab extends PluginSettingTab {
|
|||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
getSettingDefinitions(): SettingDefinitionItem[] {
|
||||
return [
|
||||
{
|
||||
name: "Enable text toolbar",
|
||||
desc: "Show a floating formatting toolbar when text is selected in the editor.",
|
||||
control: { type: "toggle", key: "enabled" },
|
||||
},
|
||||
{
|
||||
type: "group",
|
||||
heading: "Default commands",
|
||||
items: TEXT_TOOLBAR_DEFAULT_COMMANDS.map(cmd => ({
|
||||
name: cmd.name,
|
||||
desc: cmd.description,
|
||||
control: { type: "toggle", key: `command:${cmd.id}` },
|
||||
})),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Enable text toolbar")
|
||||
.setDesc("Show a floating formatting toolbar when text is selected in the editor.")
|
||||
.addToggle(t =>
|
||||
t
|
||||
.setValue(this.plugin.settings.enabled)
|
||||
.onChange(async v => {
|
||||
this.plugin.settings.enabled = v;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Default commands")
|
||||
.setHeading()
|
||||
.setDesc("Toggle which built-in formatting buttons appear in the toolbar.");
|
||||
|
||||
for (const cmd of TEXT_TOOLBAR_DEFAULT_COMMANDS) {
|
||||
new Setting(containerEl)
|
||||
.setName(cmd.name)
|
||||
.setDesc(cmd.description)
|
||||
.addToggle(t =>
|
||||
t
|
||||
.setValue(!this.plugin.settings.hideCommands.includes(cmd.id))
|
||||
.onChange(async v => {
|
||||
if (v) {
|
||||
this.plugin.settings.hideCommands = this.plugin.settings.hideCommands.filter(
|
||||
id => id !== cmd.id
|
||||
);
|
||||
} else if (!this.plugin.settings.hideCommands.includes(cmd.id)) {
|
||||
this.plugin.settings.hideCommands.push(cmd.id);
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.manager.refresh();
|
||||
})
|
||||
);
|
||||
getControlValue(key: string): unknown {
|
||||
if (key.startsWith("command:")) {
|
||||
const id = key.slice("command:".length);
|
||||
return !this.plugin.settings.hideCommands.includes(id);
|
||||
}
|
||||
return super.getControlValue(key);
|
||||
}
|
||||
|
||||
async setControlValue(key: string, value: unknown): Promise<void> {
|
||||
if (key.startsWith("command:")) {
|
||||
const id = key.slice("command:".length);
|
||||
if (value) {
|
||||
this.plugin.settings.hideCommands = this.plugin.settings.hideCommands.filter(hid => hid !== id);
|
||||
} else if (!this.plugin.settings.hideCommands.includes(id)) {
|
||||
this.plugin.settings.hideCommands.push(id);
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.manager.refresh();
|
||||
return;
|
||||
}
|
||||
await super.setControlValue(key, value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
toggleBlockquote,
|
||||
toggleCodeBlock,
|
||||
toggleInlineFormat,
|
||||
toggleWikilink,
|
||||
wrapLink,
|
||||
} from "./formatting";
|
||||
import type TextToolbarPlugin from "./main";
|
||||
|
|
@ -159,6 +160,7 @@ export default class TextToolbarManager {
|
|||
case "code": this.toggleInlineFormat("`"); break;
|
||||
case "comment": this.toggleInlineFormat("%%"); break;
|
||||
case "link": this.execLink(); return;
|
||||
case "wikilink": this.toggleWikilinkFormat(); break;
|
||||
case "code-block": this.execCodeBlock(); return;
|
||||
case "blockquote": this.execBlockquote(); return;
|
||||
}
|
||||
|
|
@ -243,6 +245,40 @@ export default class TextToolbarManager {
|
|||
this.hide();
|
||||
}
|
||||
|
||||
private toggleWikilinkFormat(): void {
|
||||
const view = this.plugin.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (!view) return;
|
||||
const editor = view.editor;
|
||||
const sel = editor.getSelection();
|
||||
if (!sel) return;
|
||||
|
||||
const from = editor.getCursor("from");
|
||||
const to = editor.getCursor("to");
|
||||
const line = editor.getLine(from.line);
|
||||
|
||||
const result = toggleWikilink({ sel, from, to, line });
|
||||
|
||||
// Case 2: brackets sit just outside the current selection — expand then replace
|
||||
if (
|
||||
from.line === to.line &&
|
||||
from.ch >= 2 && to.ch + 2 <= line.length &&
|
||||
line.substring(from.ch - 2, from.ch) === "[[" &&
|
||||
line.substring(to.ch, to.ch + 2) === "]]" &&
|
||||
result.replacement === sel
|
||||
) {
|
||||
editor.setSelection(
|
||||
{ line: from.line, ch: from.ch - 2 },
|
||||
{ line: to.line, ch: to.ch + 2 },
|
||||
);
|
||||
editor.replaceSelection(result.replacement);
|
||||
editor.setSelection(result.newFrom, result.newTo);
|
||||
return;
|
||||
}
|
||||
|
||||
editor.replaceSelection(result.replacement);
|
||||
editor.setSelection(result.newFrom, result.newTo);
|
||||
}
|
||||
|
||||
private execCopy(): void {
|
||||
const view = this.plugin.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (view) {
|
||||
|
|
|
|||
|
|
@ -9,5 +9,6 @@
|
|||
"0.1.7": "1.0.0",
|
||||
"0.1.8": "1.0.0",
|
||||
"0.2.2": "1.0.0",
|
||||
"0.2.3": "1.0.0"
|
||||
"0.2.3": "1.0.0",
|
||||
"0.2.4": "1.13.0"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue