Add exception to spellcheck for certain formats of words

What I’m trying to do

I’m trying to add an exception to Obsidian’s spellchecking feature, so that it does not flag strings with alternating letters and numbers as mistakes. Some examples are ‘03b1a12’, ‘02a5’ or ‘02c’.

I do not want to manually add an exception for each string that is formatted like this.

Things I have tried

I have tried to use a CSS snippet under settings > appearance, but I have not understood how to make an exception to the spellcheck that doesn’t turn off spellchecking completely. I feel that there could be potential here. Do you have any suggestions for how I could do this ‘general exception’ to spellcheck?

I use inline code for this kind of stuff. If you’re not familiar with it, it’s just text wrapped in backticks, which removes the red underline.

The only drawback is dealing with backticks inside the text, but I find it acceptable in my use cases.

2 Likes

You can add strings to your custom dictionary (right click on word checked and add).

I use this .ts script for searching:

import { App, Plugin, Modal, Setting } from 'obsidian';

class FormattingOptionsModal extends Modal {
	private originalSelection: string;
	private onSubmit: (searchTerm: string) => void;
	private hasBold: boolean;
	private hasBackticks: boolean;

	constructor(app: App, selection: string, onSubmit: (searchTerm: string) => void) {
		super(app);
		this.originalSelection = selection;
		this.onSubmit = onSubmit;
		this.hasBold = /\*\*[^*]+\*\*/.test(this.originalSelection);
		this.hasBackticks = /`[^`]+`/.test(this.originalSelection);
	}

	onOpen() {
		const { contentEl } = this;
		contentEl.createEl('h2', { text: 'Search Options' });
		contentEl.createEl('p', { text: 'Your selection contains markdown formatting. Choose search options:' });

		let makeBoldOptional = false;
		let makeBackticksOptional = false;

		if (this.hasBold) {
			new Setting(contentEl)
				.setName('Make bold formatting optional')
				.setDesc('Search for text with or without **bold** formatting')
				.addToggle(toggle => toggle.onChange(value => makeBoldOptional = value));
		}

		if (this.hasBackticks) {
			new Setting(contentEl)
				.setName('Make backticks optional')
				.setDesc('Search for text with or without `backtick` formatting')
				.addToggle(toggle => toggle.onChange(value => makeBackticksOptional = value));
		}

		new Setting(contentEl)
			.addButton(btn => btn
				.setButtonText('Search')
				.setCta()
				.onClick(() => {
					let searchTerm = this.originalSelection;
					console.log('Original selection:', searchTerm);

					if (!searchTerm) {
						console.warn('Empty selection passed to modal');
						this.close();
						return;
					}

					// Handle bold formatting with optional toggle
					if (this.hasBold) {
						if (makeBoldOptional) {
							searchTerm = searchTerm.replace(/\*\*(.+?)\*\*/g, (_, inner) =>
								`(\\*\\*${escapeRegex(inner)}\\*\\*|${escapeRegex(inner)})`
							);
							console.log('After bold optional:', searchTerm);
						} else {
							searchTerm = searchTerm.replace(/\*\*(.+?)\*\*/g, (_, inner) =>
								`\\*\\*${escapeRegex(inner)}\\*\\*`
							);
							console.log('After bold strict:', searchTerm);
						}
					}

					// Handle backticks formatting with optional toggle
					if (this.hasBackticks) {
						if (makeBackticksOptional) {
							searchTerm = searchTerm.replace(/`(.+?)`/g, (_, inner) =>
								`(\`${escapeRegex(inner)}\`|${escapeRegex(inner)})`
							);
							console.log('After backticks optional:', searchTerm);
						} else {
							searchTerm = searchTerm.replace(/`(.+?)`/g, (_, inner) =>
								`\`${escapeRegex(inner)}\``
							);
							console.log('After backticks strict:', searchTerm);
						}
					}

					// DO NOT escape the whole searchTerm again here!
					// It already contains regex groups for optional formatting.
					const finalSearchTerm = '/' + searchTerm;
					console.log('Final search term (modal):', finalSearchTerm);

					this.onSubmit(finalSearchTerm);
					this.close();
				}))
			.addButton(btn => btn
				.setButtonText('Cancel')
				.onClick(() => this.close()));
	}

	onClose() {
		this.contentEl.empty();
	}
}

const searchGlobally = async (app: App): Promise<void> => {
	const activeLeaf = app.workspace.getMostRecentLeaf();
	const editor = activeLeaf?.view?.editor;

	if (!editor) {
		window.open('obsidian://search?query=/');
		return;
	}

	const rawSelection = editor.getSelection();
	const selection = typeof rawSelection === 'string' ? rawSelection.trim() : '';
	console.log('[Plugin] Selection from editor:', selection);

	if (!selection) {
		window.open('obsidian://search?query=/');
		return;
	}

	const isAlreadyRegex = (text: string): boolean => /^\/.*\/[gimsuy]*$/.test(text);
	const hasPrefix = (text: string): boolean =>
		['file: ', 'path: ', 'tag: ', 'section: ', 'content: ', 'line: '].some(prefix => text.startsWith(prefix));
	const isYYMMDDFormat = (text: string): boolean =>
		/^\d{6}$/.test(text) && (() => {
			const y = parseInt(text.slice(0, 2));
			const m = parseInt(text.slice(2, 4));
			const d = parseInt(text.slice(4, 6));
			return m >= 1 && m <= 12 && d >= 1 && d <= 31;
		})();
	const hasMarkdownFormatting = (text: string): boolean =>
		/\*\*[^*]+\*\*/.test(text) || /`[^`]+`/.test(text);

	const escapeAndSearch = (query: string) => {
		window.open(`obsidian://search?query=${encodeURIComponent(query)}`);
	};

	if (hasPrefix(selection)) {
		escapeAndSearch(selection);
		return;
	}

	if (isYYMMDDFormat(selection)) {
		const escaped = escapeRegex(selection);
		escapeAndSearch(`file:"Collections Dashboard.canvas" /${escaped}`);
		return;
	}

	if (hasMarkdownFormatting(selection)) {
		// Open modal for optional markdown search
		new FormattingOptionsModal(app, selection, (searchTerm: string) => {
			escapeAndSearch(searchTerm);
		}).open();
		return;
	}

	// No markdown formatting detected: fully escape for strict literal regex
	const escaped = escapeRegex(selection);
	escapeAndSearch('/' + escaped);
};

function escapeRegex(text: string): string {
	// Escapes all special regex chars (including /) so that they are treated literally
	return text.replace(/[.*+?^${}()|[\]\\\/]/g, '\\$&');
}

export default class SearchGloballyPlugin extends Plugin {
	async onload() {
		this.addCommand({
			id: 'search-globally-custom',
			name: 'Search Globally Custom',
			callback: () => searchGlobally(this.app)
		});
	}
}

export async function invoke(app: App): Promise<void> {
	return searchGlobally(app);
}

Save it as ‘Search-Globally.ts’ and register it (well, not it, but the folder you placed it in) with Codescript Toolkit.
Assign a key combo to it and when you have text selected in your markdown, fire it.
The basic functionality is using regex for everything and if you have markdown formatting such as bold or inline code, in a modal it will make you choose what to search with (possibly both versions, if you pick that).

I used to use that when I was using OneNote but one day that data got deleted and all the redlines came back. So, I try to avoid it and implement a more futureproof solution.

However, this might not be a problem in Obsidian as it’s pretty easy to backup the entire vault.

You can sync that file to your own vault and sync it cross platform.
See idea:

1 Like