├── versions.json
├── .eslintignore
├── preview.gif
├── data.json
├── src
├── orthography
│ ├── personalDictionary
│ │ ├── index.ts
│ │ ├── personalDictionary.ts
│ │ └── personalDictionaryTab.ts
│ ├── index.ts
│ ├── UIElements
│ │ ├── UILoader.ts
│ │ ├── UIRunner.ts
│ │ ├── UIHintsFallback.ts
│ │ ├── UIBar.ts
│ │ ├── UIControls.ts
│ │ ├── UIIcons.ts
│ │ ├── UIDictionary.ts
│ │ └── UIHints.ts
│ ├── helpers
│ │ ├── debounce.ts
│ │ └── formatters.ts
│ ├── orthographyToggler.ts
│ ├── orthographyEditor.ts
│ └── orthographyPopup.ts
├── settings
│ ├── index.ts
│ ├── orthographySettings.ts
│ └── orthographySettingTab.ts
├── config.ts
├── constants.ts
├── interfaces.ts
├── cssClasses.ts
└── main.ts
├── babel.config.js
├── .gitignore
├── .prettierrc
├── jest.config.ts
├── manifest.json
├── tsconfig.json
├── rollup.config.js
├── .eslintrc
├── LICENCE
├── __tests__
└── orthography
│ └── orthographyEditor.test.ts
├── package.json
├── README.md
└── styles.css
/versions.json:
--------------------------------------------------------------------------------
1 | {
2 | "2.0.4": "0.13.31"
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/
2 | dist/
3 | node_modules/
4 | .snapshots/
5 | *.min.js
6 |
--------------------------------------------------------------------------------
/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denisoed/obsidian-orthography/HEAD/preview.gif
--------------------------------------------------------------------------------
/data.json:
--------------------------------------------------------------------------------
1 | {
2 | "displayRunner": true,
3 | "useGrammar": true,
4 | "language": "en, ru, uk"
5 | }
--------------------------------------------------------------------------------
/src/orthography/personalDictionary/index.ts:
--------------------------------------------------------------------------------
1 | export * from './personalDictionary';
2 | export * from './personalDictionaryTab';
3 |
--------------------------------------------------------------------------------
/src/settings/index.ts:
--------------------------------------------------------------------------------
1 | export * from '../settings/orthographySettings';
2 | export * from '../settings/orthographySettingTab';
3 |
--------------------------------------------------------------------------------
/src/orthography/index.ts:
--------------------------------------------------------------------------------
1 | export * from './orthographyPopup';
2 | export * from './orthographyToggler';
3 | export * from './orthographyEditor';
4 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ['@babel/preset-env', { targets: { node: 'current' } }],
4 | '@babel/preset-typescript'
5 | ]
6 | };
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Intellij
2 | *.iml
3 | .idea
4 |
5 | # npm
6 | node_modules
7 | package-lock.json
8 |
9 | # build
10 | main.js
11 | *.js.map
12 |
13 | data.json
14 | dictionary.json
15 |
16 | todo.md
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "jsxSingleQuote": true,
4 | "tabWidth": 2,
5 | "bracketSpacing": true,
6 | "jsxBracketSameLine": false,
7 | "arrowParens": "always",
8 | "trailingComma": "none"
9 | }
10 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | export const API_URL_SPELLER =
2 | 'https://speller.yandex.net/services/spellservice.json/checkText';
3 | export const API_URL_GRAMMAR =
4 | 'https://obsidian-orthography-api-mz8l64tz3-denisoed.vercel.app/check';
5 |
--------------------------------------------------------------------------------
/src/orthography/UIElements/UILoader.ts:
--------------------------------------------------------------------------------
1 | const UILoader = (): string => {
2 | const loader = `
3 |
4 | Checking...
5 |
6 | `;
7 |
8 | return loader;
9 | };
10 |
11 | export default UILoader;
12 |
--------------------------------------------------------------------------------
/src/orthography/UIElements/UIRunner.ts:
--------------------------------------------------------------------------------
1 | const UIRunner = (text: string): string => {
2 | const runner = `
3 |
6 | `;
7 |
8 | return runner;
9 | };
10 |
11 | export default UIRunner;
12 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * For a detailed explanation regarding each configuration property, visit:
3 | * https://jestjs.io/docs/configuration
4 | */
5 |
6 | module.exports = {
7 | // A map from regular expressions to paths to transformers
8 | transform: {
9 | '^.+\\.[t|j]sx?$': 'babel-jest'
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const O_RUNNER_ICON = '✦';
2 | export const O_RUNNER_ICON_CLEAR = '✕';
3 | export const O_NOT_OPEN_FILE = 'Please open a file first.';
4 | export const O_SERVER_ERROR =
5 | 'The server is not responding. Please check your Internet connection.';
6 | export const O_NO_ERROR = 'Spelling errors not found!';
7 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "obsidian-orthography",
3 | "name": "Orthography",
4 | "version": "3.1.0",
5 | "minAppVersion": "1.7.4",
6 | "description": "Check & fix orthography errors in text.",
7 | "author": "Denisoed",
8 | "authorUrl": "https://github.com/denisoed",
9 | "isDesktopOnly": false
10 | }
11 |
--------------------------------------------------------------------------------
/src/orthography/UIElements/UIHintsFallback.ts:
--------------------------------------------------------------------------------
1 | const UIHintsFallback = (): string => {
2 | const hintsFallback = `
3 |
4 |
7 |
Alpha version
8 |
9 | `;
10 |
11 | return hintsFallback;
12 | };
13 |
14 | export default UIHintsFallback;
15 |
--------------------------------------------------------------------------------
/src/orthography/helpers/debounce.ts:
--------------------------------------------------------------------------------
1 | interface DebounceCallback {
2 | apply: (ctx: any, args: any) => void;
3 | }
4 |
5 | const debounce = (callback: DebounceCallback, timeout: number): any => {
6 | let timer: any;
7 | return (...args: any[]) => {
8 | clearTimeout(timer);
9 | timer = setTimeout(() => {
10 | callback.apply(this, args);
11 | }, timeout);
12 | };
13 | };
14 |
15 | export default debounce;
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "inlineSourceMap": true,
5 | "inlineSources": true,
6 | "module": "ESNext",
7 | "target": "es6",
8 | "allowJs": true,
9 | "noImplicitAny": true,
10 | "moduleResolution": "node",
11 | "importHelpers": true,
12 | "allowSyntheticDefaultImports": true,
13 | "lib": ["dom", "es5", "scripthost", "es2015"]
14 | },
15 | "include": ["**/*.ts"]
16 | }
17 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import typescript from '@rollup/plugin-typescript';
2 | import {nodeResolve} from '@rollup/plugin-node-resolve';
3 | import commonjs from '@rollup/plugin-commonjs';
4 |
5 | export default {
6 | input: 'src/main.ts',
7 | output: {
8 | dir: '.',
9 | sourcemap: 'inline',
10 | format: 'cjs',
11 | exports: 'default'
12 | },
13 | external: ['obsidian'],
14 | plugins: [
15 | typescript(),
16 | nodeResolve({browser: true}),
17 | commonjs(),
18 | ],
19 | onwarn: function(warning) {
20 | if ( warning.code === 'THIS_IS_UNDEFINED' ) { return; }
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/src/orthography/helpers/formatters.ts:
--------------------------------------------------------------------------------
1 | import { IData } from 'src/interfaces';
2 |
3 | export const sortAlerts = (alerts: IData[]): any => {
4 | return alerts.sort((a: any, b: any) => a.begin - b.begin);
5 | };
6 |
7 | export const formatAlerts = (alerts: IData[]): any => {
8 | const withoutHidden = alerts.filter((alert: any) => alert.hidden !== true);
9 | const withoutDuplicate = withoutHidden.reduce((acc, current) => {
10 | const x = acc.find((item: any) => item.explanation === current.explanation);
11 | if (!x) {
12 | return acc.concat([current]);
13 | } else {
14 | return acc;
15 | }
16 | }, []);
17 | return withoutDuplicate;
18 | };
19 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true
4 | },
5 | "parser": "@typescript-eslint/parser",
6 | "parserOptions": {
7 | "ecmaVersion": 2020,
8 | "sourceType": "module"
9 | },
10 | "plugins": ["@typescript-eslint", "prettier"],
11 | "extends": [
12 | "eslint:recommended",
13 | "plugin:@typescript-eslint/eslint-recommended",
14 | "plugin:@typescript-eslint/recommended",
15 | "plugin:prettier/recommended",
16 | "prettier/@typescript-eslint" // needs to be last in the list
17 | ],
18 | "rules": {
19 | "no-unused-vars": "off",
20 | "@typescript-eslint/no-explicit-any": "off",
21 | "@typescript-eslint/no-unused-vars": [
22 | "error",
23 | {
24 | "argsIgnorePattern": "^_"
25 | }
26 | ]
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/orthography/UIElements/UIBar.ts:
--------------------------------------------------------------------------------
1 | import UIControls from './UIControls';
2 | import UIHints from './UIHints';
3 | import { IAlert } from '../../interfaces';
4 | import UIHintsFallback from './UIHintsFallback';
5 | import UILoader from './UILoader';
6 | import UIDictionary from './UIDictionary';
7 |
8 | const UIBar = (
9 | data: IAlert,
10 | loading: boolean,
11 | showDictionary = false,
12 | dictionary: string[] = []
13 | ): string => {
14 | const hasData = data && data.alerts && data.alerts.length;
15 | const controls: string = UIControls(!!hasData);
16 | const fallback = loading ? UILoader() : UIHintsFallback();
17 | const cards = showDictionary
18 | ? UIDictionary(dictionary)
19 | : hasData
20 | ? UIHints(data.alerts)
21 | : fallback;
22 | return `${controls}${cards}`;
23 | };
24 |
25 | export default UIBar;
26 |
--------------------------------------------------------------------------------
/src/orthography/UIElements/UIControls.ts:
--------------------------------------------------------------------------------
1 | const UIControls = (hasData: boolean): string => {
2 | return `
3 |
12 | `;
13 | };
14 |
15 | export default UIControls;
16 |
--------------------------------------------------------------------------------
/src/interfaces.ts:
--------------------------------------------------------------------------------
1 | export interface IData {
2 | impact: string;
3 | highlightText: string;
4 | minicardTitle: string;
5 | group: string;
6 | replacements: string[];
7 | explanation: string;
8 | cardLayout: { group: string };
9 | text: string;
10 | begin: number;
11 | category: string;
12 | }
13 |
14 | export interface IAlert {
15 | alerts: IData[];
16 | }
17 |
18 | export interface IOriginalWord {
19 | begin: number;
20 | end: number;
21 | len: number;
22 | }
23 |
24 | export interface IEditor {
25 | eachLine(callback: any): void;
26 | markText(
27 | from: { line: number; ch: number },
28 | to: { line: number; ch: number },
29 | oprions: any
30 | ): void;
31 | getDoc(): {
32 | replaceRange(
33 | newText: string,
34 | from: { line: number; ch: number },
35 | to: { line: number; ch: number }
36 | ): void;
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/src/orthography/UIElements/UIIcons.ts:
--------------------------------------------------------------------------------
1 | export const moveIcon =
2 | '';
3 | export const collapseIcon =
4 | '';
5 | export const horizontalSizeIcon =
6 | '';
7 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 | Copyright (c) 2021 Denisoed
3 |
4 | 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:
5 |
6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 |
8 | 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.
--------------------------------------------------------------------------------
/__tests__/orthography/orthographyEditor.test.ts:
--------------------------------------------------------------------------------
1 | import { OrthographyEditor } from '../../src/orthography/orthographyEditor';
2 |
3 | const _OrthographyEditor = new OrthographyEditor(null, null);
4 | const editor = {
5 | eachLine: () => {
6 | return;
7 | },
8 | getDoc: () => {
9 | return {
10 | replaceRange: () => {
11 | return;
12 | }
13 | };
14 | },
15 | markText: () => {
16 | return;
17 | }
18 | };
19 | const originalWord = {
20 | begin: 0,
21 | end: 5,
22 | len: 5
23 | };
24 |
25 | describe('OrthographyEditor', () => {
26 | it('should be defined', () => {
27 | expect(OrthographyEditor).toBeDefined();
28 | });
29 |
30 | describe('getColRow', () => {
31 | it('if not provide editor and originalWord args will return undefined', () => {
32 | const result = _OrthographyEditor.getColRow(undefined, undefined);
33 | expect(result).toBeUndefined();
34 | });
35 |
36 | it('if not provide originalWord args will return undefined', () => {
37 | const result = _OrthographyEditor.getColRow(editor, undefined);
38 | expect(result).toBeUndefined();
39 | });
40 |
41 | it('if not provide editor args will return undefined', () => {
42 | const result = _OrthographyEditor.getColRow(undefined, originalWord);
43 | expect(result).toBeUndefined();
44 | });
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/src/orthography/UIElements/UIDictionary.ts:
--------------------------------------------------------------------------------
1 | const UIDictionary = (dictionary: string[]): string => {
2 | if (!dictionary.length) {
3 | return `Your personal dictionary is empty.
`;
4 | }
5 |
6 | return `
7 |
8 |
Your Personal Dictionary
9 |
10 |
11 |
12 |
13 |
14 | ${dictionary
15 | .map(
16 | (word, index) => `
17 |
18 |
19 |
20 |
21 |
22 |
23 | `
24 | )
25 | .join('')}
26 |
27 |
28 | `;
29 | };
30 |
31 | export default UIDictionary;
32 |
--------------------------------------------------------------------------------
/src/cssClasses.ts:
--------------------------------------------------------------------------------
1 | // Grammer popup
2 | export const O_POPUP = 'obsidian-orthography-popup';
3 | export const O_POPUP_DISABLED = 'obsidian-orthography-popup--disabled';
4 | export const O_POPUP_CONTROLS = 'obsidian-orthography-popup-controls';
5 | export const O_POPUP_ITEM = 'obsidian-orthography-popup-item';
6 | export const O_POPUP_RESIZED = 'obsidian-orthography-popup--resized';
7 | export const O_POPUP_ITEM_OPENED = 'obsidian-orthography-popup-item--opened';
8 | export const O_POPUP_WORD_TO_REPLACE = 'obsidian-orthography-word-to-replace';
9 | export const O_POPUP_IGNORE_BUTTON = 'obsidian-orthography-ignore-button';
10 |
11 | // Runner
12 | export const O_RUNNER = 'obsidian-orthography-runner';
13 | export const O_RUNNER_ACTIVE = 'obsidian-orthography-runner--active';
14 | export const O_RUNNER_CLEAR = 'obsidian-orthography-runner--clear';
15 | export const O_RUNNER_HIDDEN = 'obsidian-orthography-runner--hidden';
16 | export const O_RUNNER_LOADING = 'obsidian-orthography-runner--loading';
17 |
18 | // Tooltip
19 | export const O_TOOLTIP = 'obsidian-orthography-tooltip';
20 | export const O_TOOLTIP_VISIBLE = 'obsidian-orthography-tooltip--visible';
21 | export const O_TOOLTIP_HINT = 'obsidian-orthography-tooltip-hint';
22 |
23 | // Highlight
24 | export const O_HIGHLIGHT = 'obsidian-orthography-highlight';
25 | export const O_HIGHLIGHT_FOCUSED = 'obsidian-orthography-highlight--focused';
26 |
27 | // Personal Dictionary
28 | export const O_DICT_WORD_CHECKBOX =
29 | 'obsidian-orthography-dictionary-word-checkbox';
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "obsidian-orthography",
3 | "version": "3.1.0",
4 | "description": "Check orthography plugin for Obsidian (https://obsidian.md)",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/denisoed/obsidian-orthography"
8 | },
9 | "main": "main.js",
10 | "scripts": {
11 | "dev": "rollup --config rollup.config.js -w",
12 | "build": "npm run lint && rollup --config rollup.config.js",
13 | "lint": "eslint '*/**/*.{js,ts}'",
14 | "test": "jest",
15 | "format": "prettier --write \"src/**/*.ts\""
16 | },
17 | "keywords": [
18 | "obsidian",
19 | "obsidian-md",
20 | "obsidian-md-plugin"
21 | ],
22 | "author": "Denisoed",
23 | "license": "MIT",
24 | "engines": {
25 | "node": "16.20.2",
26 | "npm": "8.19.4"
27 | },
28 | "devDependencies": {
29 | "@babel/preset-env": "^7.16.11",
30 | "@babel/preset-typescript": "^7.16.7",
31 | "@rollup/plugin-commonjs": "^15.1.0",
32 | "@rollup/plugin-node-resolve": "^9.0.0",
33 | "@rollup/plugin-typescript": "^6.1.0",
34 | "@types/jest": "^27.4.1",
35 | "@types/node": "^14.14.2",
36 | "@typescript-eslint/eslint-plugin": "^4.13.0",
37 | "@typescript-eslint/parser": "^4.13.0",
38 | "babel-jest": "^27.5.1",
39 | "eslint": "^7.18.0",
40 | "eslint-config-prettier": "^7.1.0",
41 | "eslint-plugin-import": "^2.22.1",
42 | "eslint-plugin-prettier": "^3.3.1",
43 | "jest": "^27.5.1",
44 | "obsidian": "^1.5.7-1",
45 | "prettier": "^2.2.1",
46 | "rollup": "^2.32.1",
47 | "ts-jest": "^27.1.3",
48 | "ts-node": "^10.9.2",
49 | "tslib": "^2.0.4",
50 | "typescript": "^4.0.3"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/settings/orthographySettings.ts:
--------------------------------------------------------------------------------
1 | import type { Events } from 'obsidian';
2 | import type OrthographyPlugin from '../main';
3 |
4 | interface SettingsData {
5 | displayRunner: boolean;
6 | useGrammar: boolean;
7 | language: string;
8 | }
9 |
10 | function getDefaultData(): SettingsData {
11 | return {
12 | displayRunner: true,
13 | useGrammar: false,
14 | language: 'en, ru, uk'
15 | };
16 | }
17 |
18 | export class OrthographySettings {
19 | private data: SettingsData;
20 | private emitter: any;
21 |
22 | constructor(private plugin: OrthographyPlugin, emitter: Events) {
23 | this.data = getDefaultData();
24 | this.emitter = emitter;
25 | }
26 |
27 | get displayRunner(): boolean {
28 | const { data } = this;
29 | return data.displayRunner;
30 | }
31 |
32 | set displayRunner(value: boolean) {
33 | const { data } = this;
34 | data.displayRunner = value;
35 | this.emitter.trigger('onUpdateSettings', this.data);
36 | }
37 |
38 | get useGrammar(): boolean {
39 | const { data } = this;
40 | return data.useGrammar;
41 | }
42 |
43 | set useGrammar(value: boolean) {
44 | const { data } = this;
45 | data.useGrammar = value;
46 | this.emitter.trigger('onUpdateSettings', this.data);
47 | }
48 |
49 | get language(): string {
50 | const { data } = this;
51 | return data.language;
52 | }
53 |
54 | set language(value: string) {
55 | const { data } = this;
56 | data.language = value;
57 | this.emitter.trigger('onUpdateSettings', this.data);
58 | }
59 |
60 | async loadSettings(): Promise {
61 | const { plugin } = this;
62 | this.data = Object.assign(getDefaultData(), await plugin.loadData());
63 | }
64 |
65 | async saveSettings(): Promise {
66 | const { plugin, data } = this;
67 | if (plugin && data) {
68 | await plugin.saveData(data);
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/settings/orthographySettingTab.ts:
--------------------------------------------------------------------------------
1 | import { App, PluginSettingTab, Setting } from 'obsidian';
2 | import { OrthographySettings } from 'src/settings';
3 | import type OrthographyPlugin from '../main';
4 |
5 | export class OrthographySettingTab extends PluginSettingTab {
6 | constructor(
7 | app: App,
8 | private settings: OrthographySettings,
9 | plugin: OrthographyPlugin
10 | ) {
11 | super(app, plugin);
12 | }
13 |
14 | display(): void {
15 | const { containerEl, settings } = this;
16 |
17 | containerEl.empty();
18 | OrthographySettingTab.setDisplayRunner(containerEl, settings);
19 | OrthographySettingTab.setGrammar(containerEl, settings);
20 | OrthographySettingTab.setLanguage(containerEl, settings);
21 | }
22 |
23 | static setDisplayRunner(
24 | containerEl: HTMLElement,
25 | settings: OrthographySettings
26 | ): void {
27 | new Setting(containerEl)
28 | .setName('Show button')
29 | .setDesc('Button for orthography checking')
30 | .addToggle((toggle) =>
31 | toggle.setValue(settings.displayRunner).onChange((value) => {
32 | settings.displayRunner = value;
33 | settings.saveSettings();
34 | })
35 | );
36 | }
37 |
38 | static setGrammar(
39 | containerEl: HTMLElement,
40 | settings: OrthographySettings
41 | ): void {
42 | new Setting(containerEl)
43 | .setName('Grammarly')
44 | .setDesc('Use grammarly to find and correct errors.')
45 | .addToggle((toggle) =>
46 | toggle.setValue(settings.useGrammar).onChange((value) => {
47 | settings.useGrammar = value;
48 | settings.saveSettings();
49 | })
50 | );
51 | }
52 |
53 | static setLanguage(
54 | containerEl: HTMLElement,
55 | settings: OrthographySettings
56 | ): void {
57 | new Setting(containerEl)
58 | .setName('Language setting')
59 | .setDesc('Select language')
60 | .addDropdown((dropdown) =>
61 | dropdown
62 | .addOption('en', 'English')
63 | .addOption('ru', 'Russian')
64 | .addOption('uk', 'Ukraine')
65 | .addOption('en, ru, uk', 'All')
66 | .onChange(async (value) => {
67 | settings.language = value;
68 | await settings.saveSettings();
69 | })
70 | );
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Obsidian Orthography
2 |
3 | **THE PLUGIN DOES NOT WORK CORRECTLY IN THE "LIVE PREVIEW" MODE :(**
4 |
5 | **I'LL FIGURE IT OUT AND FIX IT**
6 |
7 | [](https://maddevs.io/)
8 |
9 | 
10 |
11 | 
12 |
13 |
14 | The Obsidian plugin for checking grammar and correcting spelling errors in text.
15 |
16 | 
17 |
18 | **IMPORTANT**:
19 |
20 | 1. **ALPHA version**.
21 | 2. The plugin does not work correctly in the "Live Preview" mode. I'll figure it out and fix it
22 | 3. This is experimental and may have instability. It is possible that there are bugs which may delete data in the current note. Please make backups!
23 | 4. The plugin handles only English text.
24 |
25 | ## Features
26 |
27 | - Search for spelling errors in the text
28 | - Displaying options for correcting word mistakes
29 | - Correct a word mistake in one click
30 |
31 | ## Todo
32 |
33 | - [x] Integration [Grammarly](https://www.grammarly.com)
34 | - [ ] Support "Live Preview" mode
35 | - [ ] Fix the styles in the mobile version
36 | - [x] Add the ability to save words in your personal dictionary
37 | - [ ] Add settings for selecting a dialect(`american` or `british`)
38 | - [ ] Add Plagiarism Checker
39 |
40 | ## For Developers
41 |
42 | ### How to get started developing
43 |
44 | 1. Create new vault(folder) in Obsidian. For example `Orthography`
45 |
46 | 2. In the terminal, go to the `./Orthography/.obsidian` folder and create there the `plugins` folder
47 |
48 | 3. Clone the repository into the `plugins` folder: `git clone https://github.com/denisoed/obsidian-orthography.git`
49 |
50 | 4. Install dependencies: `npm i`
51 |
52 | 5. And run for develop: `npm run dev`
53 |
54 | ## Pricing
55 |
56 | This plugin is provided to everyone for free, however if you would like to
57 | say thanks or help support continued development, feel free to send a little
58 | my way through one of the following methods:
59 |
60 | [
](https://www.buymeacoffee.com/denisoed)
61 |
--------------------------------------------------------------------------------
/src/orthography/personalDictionary/personalDictionary.ts:
--------------------------------------------------------------------------------
1 | import { App, Notice } from 'obsidian';
2 |
3 | export class PersonalDictionary {
4 | private static instance: PersonalDictionary | null = null;
5 | private data: { dictionary: string[] } = { dictionary: [] };
6 | private category = 'Misspelled';
7 | private app: App;
8 |
9 | constructor(app: App) {
10 | this.app = app;
11 | }
12 |
13 | get dictionary(): string[] {
14 | return this.data.dictionary || [];
15 | }
16 |
17 | async loadDictionary(): Promise {
18 | const filePath = this.getDictionaryFilePath();
19 |
20 | try {
21 | if (await this.app.vault.adapter.exists(filePath)) {
22 | const storedData = await this.app.vault.adapter.read(filePath);
23 | const parsedData = JSON.parse(storedData);
24 |
25 | if (parsedData && Array.isArray(parsedData.dictionary)) {
26 | this.data.dictionary = parsedData.dictionary;
27 | } else {
28 | this.data.dictionary = [];
29 | }
30 | } else {
31 | this.data.dictionary = [];
32 | await this.saveDictionary();
33 | }
34 | } catch (error) {
35 | new Notice('Error loading personal dictionary');
36 | }
37 | }
38 |
39 | async addWord(word: string): Promise {
40 | word = word.toLowerCase();
41 |
42 | if (!this.data.dictionary.includes(word)) {
43 | this.data.dictionary.push(word);
44 | await this.saveDictionary();
45 | }
46 | }
47 |
48 | async remove(wordsToRemove: string[]): Promise {
49 | this.data.dictionary = this.data.dictionary.filter((word) => {
50 | word = word.toLowerCase();
51 | return !wordsToRemove.includes(word);
52 | });
53 | await this.saveDictionary();
54 | }
55 |
56 | filterAlerts(alerts: any[]): any[] {
57 | return alerts.filter((alert: any) => {
58 | if (alert && alert.text && alert.category) {
59 | if (alert.category === this.category) {
60 | return !this.containsWord(alert.text);
61 | }
62 | }
63 | return true;
64 | });
65 | }
66 |
67 | private containsWord(word: string): boolean {
68 | return this.data.dictionary.includes(word.toLowerCase());
69 | }
70 |
71 | private getDictionaryFilePath(): string {
72 | return `${this.app.vault.configDir}/plugins/obsidian-orthography/dictionary.json`;
73 | }
74 |
75 | private async saveDictionary(): Promise {
76 | const filePath = this.getDictionaryFilePath();
77 |
78 | try {
79 | const dataToSave = JSON.stringify(
80 | { dictionary: this.data.dictionary },
81 | null,
82 | 1
83 | );
84 | await this.app.vault.adapter.write(filePath, dataToSave);
85 | } catch (error) {
86 | new Notice('Error saving personal dictionary');
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/orthography/personalDictionary/personalDictionaryTab.ts:
--------------------------------------------------------------------------------
1 | import { PersonalDictionary } from './personalDictionary';
2 | import { OrthographyPopup } from '../orthographyPopup';
3 | import { O_DICT_WORD_CHECKBOX } from '../../cssClasses';
4 |
5 | interface IPersonalDictionaryTab {
6 | init(): void;
7 | }
8 |
9 | let self: any;
10 |
11 | export class PersonalDictionaryTab implements IPersonalDictionaryTab {
12 | private dictionary: PersonalDictionary;
13 | private removeSelectedBtn: any;
14 | private selectAllBtn: any;
15 | private orthographyPopup: OrthographyPopup;
16 |
17 | constructor(
18 | orthographyPopup: OrthographyPopup,
19 | dictionary: PersonalDictionary
20 | ) {
21 | this.orthographyPopup = orthographyPopup;
22 | this.dictionary = dictionary;
23 |
24 | this.init();
25 | }
26 |
27 | public init(): void {
28 | self = this;
29 | }
30 |
31 | create() {
32 | self.setListeners();
33 | }
34 |
35 | destroy() {
36 | self.removeListeners();
37 | }
38 |
39 | update() {
40 | self.removeListeners();
41 | self.setListeners();
42 | }
43 |
44 | setListeners(): void {
45 | self.selectAllBtn = document.getElementById('select-all-button');
46 | if (self.selectAllBtn) {
47 | self.selectAllBtn.addEventListener(
48 | 'click',
49 | self.onSelectAllCheckboxes.bind(self)
50 | );
51 | }
52 |
53 | self.removeSelectedBtn = document.getElementById('remove-selected-button');
54 | if (self.removeSelectedBtn) {
55 | self.removeSelectedBtn.addEventListener(
56 | 'click',
57 | self.onRemoveSelected.bind(self)
58 | );
59 | }
60 | }
61 |
62 | removeListeners(): void {
63 | self.selectAllBtn = document.getElementById('select-all-button');
64 | if (self.selectAllBtn) {
65 | self.selectAllBtn.removeEventListener(
66 | 'click',
67 | self.onSelectAllCheckboxes.bind(self)
68 | );
69 | }
70 |
71 | self.removeSelectedBtn = document.getElementById(
72 | 'obsidian-orthography-remove-selected-button'
73 | );
74 | if (self.removeSelectedBtn) {
75 | self.removeSelectedBtn.removeEventListener(
76 | 'click',
77 | self.onRemoveSelected.bind(self)
78 | );
79 | }
80 | }
81 |
82 | private onSelectAllCheckboxes() {
83 | const checkboxes = document.querySelectorAll(`.${O_DICT_WORD_CHECKBOX}`);
84 | const allChecked = Array.from(checkboxes).every(
85 | (checkbox: HTMLInputElement) => checkbox.checked
86 | );
87 |
88 | checkboxes.forEach((checkbox: HTMLInputElement) => {
89 | checkbox.checked = !allChecked;
90 | });
91 | }
92 |
93 | private onRemoveSelected() {
94 | const checkboxes = document.querySelectorAll(
95 | `.${O_DICT_WORD_CHECKBOX}:checked`
96 | );
97 | const wordsToRemove = Array.from(checkboxes).map(
98 | (checkbox: HTMLInputElement) => checkbox.value
99 | );
100 | self.dictionary.remove(wordsToRemove);
101 | self.orthographyPopup.update(null, false, true);
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/orthography/orthographyToggler.ts:
--------------------------------------------------------------------------------
1 | import { Events, Notice } from 'obsidian';
2 | import type { App } from 'obsidian';
3 | import { OrthographySettings } from '../settings';
4 | import {
5 | O_RUNNER_ICON,
6 | O_RUNNER_ICON_CLEAR,
7 | O_NOT_OPEN_FILE
8 | } from '../constants';
9 | import { O_RUNNER, O_RUNNER_HIDDEN, O_RUNNER_LOADING } from '../cssClasses';
10 |
11 | interface IOrthographyToggler {
12 | init(): void;
13 | }
14 |
15 | let self: any;
16 |
17 | export class OrthographyToggler implements IOrthographyToggler {
18 | private app: App;
19 | private settings: OrthographySettings;
20 | private emitter: any;
21 | private toggler: any;
22 | private showed: false;
23 |
24 | constructor(app: App, settings: OrthographySettings, emitter: Events) {
25 | this.app = app;
26 | this.settings = settings;
27 | this.emitter = emitter;
28 | }
29 |
30 | public init(): void {
31 | self = this;
32 | this.createButton(O_RUNNER_ICON);
33 | }
34 |
35 | public destroy(): void {
36 | this.removeLoading();
37 | this.toggler.removeEventListener('click', this.toggle);
38 | this.removeButton();
39 | }
40 |
41 | public toggle(): void {
42 | const activeEditor = self.getEditor();
43 | if (!activeEditor) {
44 | if (self.showed) {
45 | self.setButtonWithRunner();
46 | self.showed = false;
47 | } else {
48 | new Notice(O_NOT_OPEN_FILE);
49 | }
50 | return;
51 | }
52 | self.showed = !self.showed;
53 | if (self.showed) {
54 | self.setButtonWithClear();
55 | } else {
56 | self.setButtonWithRunner();
57 | }
58 | }
59 |
60 | public hide(): void {
61 | const runner = document.querySelector('.' + O_RUNNER);
62 | runner.classList.add(O_RUNNER_HIDDEN);
63 | }
64 |
65 | public setLoading(): void {
66 | this.toggler.classList.add(O_RUNNER_LOADING);
67 | }
68 |
69 | public removeLoading(): void {
70 | this.toggler.classList.remove(O_RUNNER_LOADING);
71 | }
72 |
73 | public reset(): void {
74 | this.showed = false;
75 | this.removeLoading();
76 | this.updateButtonText(O_RUNNER_ICON);
77 | }
78 |
79 | private createButton(text: string) {
80 | this.toggler = document.createElement('button');
81 | const icon = document.createElement('span');
82 | icon.innerText = text;
83 | this.toggler.classList.add(O_RUNNER);
84 | this.toggler.appendChild(icon);
85 | document.body.appendChild(this.toggler);
86 | this.toggler.addEventListener('click', this.toggle);
87 | }
88 |
89 | private updateButtonText(text: string) {
90 | const toggler: HTMLElement = document.querySelector(`.${O_RUNNER} span`);
91 | if (toggler) toggler.innerText = text;
92 | }
93 |
94 | private removeButton() {
95 | const toggler: HTMLElement = document.querySelector(`.${O_RUNNER}`);
96 | if (toggler) toggler.remove();
97 | }
98 |
99 | private setButtonWithClear() {
100 | self.updateButtonText(O_RUNNER_ICON_CLEAR);
101 | self.emitter.trigger('orthography:open');
102 | }
103 |
104 | private setButtonWithRunner() {
105 | self.updateButtonText(O_RUNNER_ICON);
106 | self.removeLoading();
107 | self.emitter.trigger('orthography:close');
108 | }
109 |
110 | private getEditor() {
111 | const activeLeaf: any = this.app.workspace.activeLeaf;
112 | const sourceMode = activeLeaf.view.sourceMode;
113 | if (!sourceMode) return null;
114 | return activeLeaf.view.sourceMode.cmEditor;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/orthography/orthographyEditor.ts:
--------------------------------------------------------------------------------
1 | import { OrthographySettings } from '../settings';
2 | import type { App, Editor } from 'obsidian';
3 | import { O_HIGHLIGHT, O_HIGHLIGHT_FOCUSED } from '../cssClasses';
4 | import { IOriginalWord, IData } from 'src/interfaces';
5 |
6 | interface IOrthographyEditor {
7 | init(): void;
8 | }
9 |
10 | interface IGetColRowResult {
11 | col: number;
12 | row: number;
13 | }
14 |
15 | export class OrthographyEditor implements IOrthographyEditor {
16 | private app: App;
17 | private settings: OrthographySettings;
18 | private editor: Editor;
19 |
20 | constructor(app: App, settings: OrthographySettings, editor: Editor) {
21 | this.app = app;
22 | this.settings = settings;
23 | this.editor = editor;
24 | }
25 |
26 | public init(): void {
27 | // init
28 | }
29 |
30 | public destroy(): void {
31 | this.clearHighlightWords();
32 | }
33 |
34 | public highlightWords(alerts: IData[]): void {
35 | this.clearHighlightWords();
36 |
37 | if (!this.editor || !alerts || alerts.length === 0) return;
38 |
39 | alerts.forEach((alert: any) => {
40 | const textLength = alert.text.length || alert.highlightText.length;
41 | const originalWord = {
42 | begin: alert.begin,
43 | end: alert.end,
44 | len: textLength
45 | };
46 | this.highlightWord(originalWord);
47 | });
48 | }
49 |
50 | private highlightWord(originalWord: {
51 | begin: number;
52 | end: number;
53 | len: number;
54 | }): void {
55 | if (!this.editor || !originalWord) return;
56 | const colRow = this.getColRow(originalWord);
57 |
58 | if (!colRow) return;
59 | const { col, row } = colRow;
60 |
61 | this.editor.addHighlights(
62 | [
63 | {
64 | from: {
65 | line: row,
66 | ch: col
67 | },
68 | to: {
69 | line: row,
70 | ch: col + originalWord.len
71 | }
72 | }
73 | ],
74 | `${O_HIGHLIGHT} begin-${originalWord.begin}`
75 | );
76 | }
77 |
78 | public replaceWord(originalWord: IOriginalWord, newWord: string): void {
79 | if (!this.editor || !originalWord || !newWord) return;
80 | const colRow = this.getColRow(originalWord);
81 | if (!colRow) return;
82 | const { col, row } = colRow;
83 |
84 | const doc = this.editor.getDoc();
85 |
86 | const from = {
87 | line: row,
88 | ch: col
89 | };
90 | const to = {
91 | line: row,
92 | ch: col + originalWord.len
93 | };
94 |
95 | doc.replaceRange(newWord, from, to);
96 | }
97 |
98 | getColRow(originalWord: IOriginalWord): IGetColRowResult {
99 | if (!this.editor || !originalWord) return;
100 |
101 | let ttl = 0;
102 | let row = 0;
103 | let result;
104 | const { begin } = originalWord;
105 |
106 | const lines = this.editor.lineCount();
107 |
108 | for (let i = 0; i < lines; i++) {
109 | const lineText = this.editor.getLine(i);
110 | const s = ttl === 0 ? ttl : ttl + 1;
111 | const lineTextLength = lineText.length;
112 | ttl += lineTextLength;
113 |
114 | if (row > 0) {
115 | ttl++;
116 | }
117 | if (begin >= s && begin <= ttl) {
118 | const diff = ttl - lineTextLength;
119 | const col = begin - diff;
120 | result = { col, row };
121 | }
122 | row++;
123 | }
124 | return result;
125 | }
126 |
127 | private clearHighlightWords(): void {
128 | const highlightWords = document.querySelectorAll(`.${O_HIGHLIGHT}`);
129 | highlightWords.forEach((span) => {
130 | this.editor.removeHighlights(span.className);
131 | });
132 | }
133 |
134 | private clearHighlightWord(word: string): void {
135 | const highlightWords = document.querySelectorAll(`.${O_HIGHLIGHT}`);
136 |
137 | highlightWords.forEach((span) => {
138 | if (span.innerText === word) {
139 | span.classList.remove(O_HIGHLIGHT);
140 | span.classList.remove(O_HIGHLIGHT_FOCUSED);
141 | }
142 | });
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/orthography/UIElements/UIHints.ts:
--------------------------------------------------------------------------------
1 | import { IData } from '../../interfaces';
2 |
3 | const JOIN_BY = 'or ';
4 |
5 | const renderHints = (card: IData, index: number): string => {
6 | const { replacements, text, begin, highlightText } = card;
7 | if (card.category === 'Determiners') {
8 | return replacements
9 | .map((item: string) => {
10 | return `
11 | `;
20 | })
21 | .join(JOIN_BY);
22 | }
23 | // ----------- FOR REMOVE HINTS ----------- //
24 | if (
25 | card.category === 'Formatting' ||
26 | card.category === 'BasicPunct' ||
27 | card.category === 'Wordiness' ||
28 | card.category === 'Conjunctions'
29 | ) {
30 | return `
31 |
39 | `;
40 | }
41 | if (card.category === 'Prepositions') {
42 | return replacements
43 | .map((item: string) => {
44 | return `
45 | `;
55 | })
56 | .join(JOIN_BY);
57 | }
58 | return replacements
59 | .map((item: string) => {
60 | return `
61 |
62 | `;
72 | })
73 | .join(JOIN_BY);
74 | };
75 |
76 | const ignoreButton = (card: IData, index: number): string => {
77 | const { category, text, begin } = card;
78 | const isMisspelled = category === 'Misspelled';
79 | return isMisspelled
80 | ? ``
87 | : '';
88 | };
89 |
90 | const UIHints = (alerts: IData[]): string => {
91 | if (!alerts || !alerts.length) return '';
92 | return alerts
93 | .map((card: IData, index: number) => {
94 | const {
95 | impact,
96 | highlightText,
97 | minicardTitle,
98 | explanation,
99 | cardLayout,
100 | begin
101 | } = card;
102 | return `
103 |
128 | `;
129 | })
130 | .join('');
131 | };
132 |
133 | export default UIHints;
134 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { Plugin, Events, Notice, MarkdownView, type Editor } from 'obsidian';
2 | import { OrthographySettings } from './settings';
3 | import {
4 | OrthographyEditor,
5 | OrthographyPopup,
6 | OrthographyToggler
7 | } from './orthography';
8 | import debounce from './orthography/helpers/debounce';
9 | import { sortAlerts, formatAlerts } from './orthography/helpers/formatters';
10 | import { API_URL_GRAMMAR } from './config';
11 | import { O_NOT_OPEN_FILE, O_SERVER_ERROR, O_NO_ERROR } from './constants';
12 | import { PersonalDictionary } from './orthography/personalDictionary';
13 |
14 | // Use self in events callbacks
15 | let self: any;
16 |
17 | export default class OrthographyPlugin extends Plugin {
18 | private settings: OrthographySettings;
19 | private popup: any;
20 | private toggler: any;
21 | private editor: any;
22 | private emitter: any;
23 | private activeEditor: Editor;
24 | private aborter: any;
25 | private hints: any;
26 | private debounceGetDataFunc = debounce(this.onChangeText.bind(this), 500);
27 | private getDataFunc = debounce(this.onRunFromPopup.bind(this), 0);
28 | private personalDictionary: PersonalDictionary;
29 |
30 | async onload(): Promise {
31 | // ------ Init -------- //
32 | self = this;
33 | this.emitter = new Events();
34 |
35 | const settings = new OrthographySettings(this, this.emitter);
36 | await settings.loadSettings();
37 | this.settings = settings;
38 |
39 | // this.addSettingTab(new OrthographySettingTab(this.app, settings, this));
40 |
41 | const personalDictionary = new PersonalDictionary(this.app);
42 | await personalDictionary.loadDictionary();
43 | this.personalDictionary = personalDictionary;
44 |
45 | // ------- Events -------- //
46 | this.emitter.on('orthography:open', this.onPopupOpen);
47 | this.emitter.on('orthography:close', this.onPopupClose);
48 | this.emitter.on('orthography:run', this.getDataFunc);
49 | this.emitter.on('orthography:replace', this.onReplaceWord);
50 | this.emitter.on('orthography:ignore', this.onIgnore);
51 | // Listen to changes in the editor
52 | this.app.workspace.on('editor-change', this.debounceGetDataFunc);
53 |
54 | // Init orthography
55 | this.app.workspace.onLayoutReady(() => {
56 | this.activeEditor = this.getEditor();
57 | this.initOrthographyToggler();
58 | this.initOrthographyPopup();
59 | this.initOrthographyEditor();
60 | });
61 | }
62 |
63 | onunload(): void {
64 | this.emitter.off('orthography:open', this.onPopupOpen);
65 | this.emitter.off('orthography:close', this.onPopupClose);
66 | this.emitter.off('orthography:run', this.onRunFromPopup);
67 | this.emitter.off('orthography:replace', this.onReplaceWord);
68 | this.emitter.off('orthography:ignore', this.onIgnore);
69 | this.app.workspace.off('editor-change', this.debounceGetDataFunc);
70 | this.toggler.destroy();
71 | this.popup.destroy();
72 | this.editor.destroy();
73 | this.hints = null;
74 | this.activeEditor = null;
75 | this.personalDictionary = null;
76 | }
77 |
78 | private initOrthographyToggler(): void {
79 | const { app, settings, emitter } = this;
80 | this.toggler = new OrthographyToggler(app, settings, emitter);
81 | this.toggler.init();
82 | }
83 |
84 | private initOrthographyPopup(): void {
85 | const { app, settings, emitter } = this;
86 | this.popup = new OrthographyPopup(
87 | app,
88 | settings,
89 | emitter,
90 | this.personalDictionary
91 | );
92 | this.popup.init();
93 | }
94 |
95 | private initOrthographyEditor(): void {
96 | const { app, settings } = this;
97 | this.editor = new OrthographyEditor(app, settings, this.activeEditor);
98 | this.editor.init();
99 | }
100 |
101 | private getEditor() {
102 | const activeLeaf = this.app.workspace.getActiveViewOfType(MarkdownView);
103 | return activeLeaf?.sourceMode?.cmEditor;
104 | }
105 |
106 | private async onChangeText() {
107 | if (!this.popup.created) return;
108 | this.runChecker();
109 | }
110 |
111 | private async onRunFromPopup() {
112 | if (!this.popup.created) return;
113 | this.editor.destroy();
114 | this.popup.setLoader();
115 | this.activeEditor = this.getEditor();
116 | if (this.activeEditor) {
117 | this.runChecker();
118 | } else {
119 | new Notice(O_NOT_OPEN_FILE);
120 | this.onPopupClose();
121 | }
122 | }
123 |
124 | private async runChecker() {
125 | this.toggler.setLoading();
126 | if (!this.activeEditor) return;
127 | const text = this.activeEditor.getValue();
128 | this.hints = await this.fetchData(text);
129 | if (this.hints instanceof TypeError) {
130 | this.popup.removeLoader();
131 | this.toggler.removeLoading();
132 | new Notice(O_SERVER_ERROR);
133 | return;
134 | }
135 | if (this.hints && this.hints.alerts && this.hints.alerts.length) {
136 | const alerts = formatAlerts(
137 | this.personalDictionary.filterAlerts(this.hints.alerts)
138 | );
139 | this.editor.highlightWords(alerts);
140 | this.popup.update({
141 | alerts: sortAlerts(alerts)
142 | });
143 | } else {
144 | new Notice(O_NO_ERROR);
145 | this.popup.removeLoader();
146 | }
147 | this.toggler.removeLoading();
148 | }
149 |
150 | private onPopupOpen() {
151 | self.popup.create();
152 | }
153 |
154 | private onPopupClose() {
155 | self.editor.destroy();
156 | self.popup.destroy();
157 | self.toggler.reset();
158 | if (self.aborter) {
159 | self.aborter.abort();
160 | self.aborter = null;
161 | }
162 | }
163 |
164 | private onReplaceWord(event: any) {
165 | const origWordLen = event.currentTarget.dataset.text.length;
166 | const newWord = event.currentTarget.dataset.toreplace;
167 | const begin = event.currentTarget.dataset.begin;
168 | const end = begin + origWordLen;
169 | self.editor.replaceWord(
170 | {
171 | begin: +begin,
172 | end: +end,
173 | len: +origWordLen
174 | },
175 | newWord
176 | );
177 | }
178 |
179 | private onIgnore(event: any) {
180 | const word = event.currentTarget.dataset.text;
181 | self.personalDictionary.addWord(word);
182 | self.editor.clearHighlightWord(word);
183 | }
184 |
185 | private async fetchData(text: string): Promise {
186 | if (self.aborter) self.aborter.abort();
187 | self.popup.disable();
188 |
189 | self.aborter = new AbortController();
190 | const { signal } = self.aborter;
191 |
192 | const url: any = new URL(API_URL_GRAMMAR);
193 | const params: any = { text };
194 | Object.keys(params).forEach((key) =>
195 | url.searchParams.append(key, params[key])
196 | );
197 | try {
198 | const response = await fetch(url, {
199 | method: 'GET',
200 | signal
201 | });
202 | self.aborter = null;
203 | return await response.json();
204 | } catch (error) {
205 | return error;
206 | } finally {
207 | self.popup.enable();
208 | }
209 | }
210 | }
211 |
--------------------------------------------------------------------------------
/src/orthography/orthographyPopup.ts:
--------------------------------------------------------------------------------
1 | import { App, Events, Notice } from 'obsidian';
2 | import { OrthographySettings } from 'src/settings';
3 | import {
4 | O_POPUP,
5 | O_POPUP_DISABLED,
6 | O_POPUP_CONTROLS,
7 | O_POPUP_ITEM,
8 | O_POPUP_RESIZED,
9 | O_POPUP_ITEM_OPENED,
10 | O_POPUP_WORD_TO_REPLACE,
11 | O_HIGHLIGHT_FOCUSED,
12 | O_POPUP_IGNORE_BUTTON
13 | } from '../cssClasses';
14 | import { O_NOT_OPEN_FILE } from '../constants';
15 | import { IAlert } from '../interfaces';
16 |
17 | import UIBar from './UIElements/UIBar';
18 | import {
19 | PersonalDictionary,
20 | PersonalDictionaryTab
21 | } from './personalDictionary';
22 |
23 | let self: any;
24 |
25 | export class OrthographyPopup {
26 | private app: App;
27 | private settings: OrthographySettings;
28 | private emitter: any;
29 | private sizer: any;
30 | private mover: any;
31 | private closer: any;
32 | private reloader: any;
33 | private runner: any;
34 | private checker: any;
35 | private dictionaryOpener: any;
36 | private popupOffset: number[] = [0, 0];
37 | private moverSelected = false;
38 | private created = false;
39 | private personalDictionary: PersonalDictionary;
40 | private personalDictionaryTab: PersonalDictionaryTab;
41 |
42 | constructor(
43 | app: App,
44 | settings: OrthographySettings,
45 | emitter: Events,
46 | personalDictionary: PersonalDictionary
47 | ) {
48 | this.app = app;
49 | this.settings = settings;
50 | this.emitter = emitter;
51 | this.personalDictionary = personalDictionary;
52 | this.personalDictionaryTab = new PersonalDictionaryTab(
53 | this,
54 | personalDictionary
55 | );
56 | }
57 |
58 | public init(): void {
59 | self = this;
60 | }
61 |
62 | public create(): void {
63 | self.created = true;
64 | self.popup = document.createElement('div');
65 | self.popup.classList.add(O_POPUP);
66 | self.popup.id = O_POPUP;
67 | const bar = UIBar(null, false);
68 | self.popup.innerHTML = bar;
69 | document.body.appendChild(self.popup);
70 | self.setListeners();
71 | }
72 |
73 | public destroy(): void {
74 | self.created = false;
75 | self.removeListeners();
76 | self.personalDictionaryTab.destroy();
77 | const popup = document.getElementById(O_POPUP);
78 | if (popup) popup.remove();
79 | }
80 |
81 | public update(data: IAlert, loading?: boolean, showDictionary = false): void {
82 | self.removeListeners();
83 | const dictionary = this.personalDictionary
84 | ? this.personalDictionary.dictionary
85 | : [];
86 | const bar = UIBar(data, loading, showDictionary, dictionary);
87 | self.popup.innerHTML = bar;
88 | self.setListeners();
89 | showDictionary
90 | ? self.personalDictionaryTab.update()
91 | : self.personalDictionaryTab.destroy();
92 | }
93 |
94 | public setLoader(): void {
95 | this.update(null, true);
96 | }
97 |
98 | public removeLoader(): void {
99 | this.update(null, false);
100 | }
101 |
102 | public disable(): void {
103 | const hints = document.querySelector(`#${O_POPUP}`);
104 | if (hints) {
105 | hints.classList.add(O_POPUP_DISABLED);
106 | }
107 | }
108 |
109 | public enable(): void {
110 | const hints = document.querySelector(`#${O_POPUP}`);
111 | if (hints) {
112 | hints.classList.remove(O_POPUP_DISABLED);
113 | }
114 | }
115 |
116 | private setListeners() {
117 | const minicards = document.querySelectorAll(`.${O_POPUP_ITEM}`);
118 | minicards.forEach((mc) => mc.addEventListener('click', self.onClickByHint));
119 | minicards.forEach((mc) =>
120 | mc.addEventListener('mouseover', self.onFocusWord)
121 | );
122 | minicards.forEach((mc) =>
123 | mc.addEventListener('mouseout', self.onRemoveFocusWord)
124 | );
125 | const replacements = document.querySelectorAll(
126 | `.${O_POPUP_WORD_TO_REPLACE}`
127 | );
128 | replacements.forEach((rp) =>
129 | rp.addEventListener('click', self.onReplaceWord)
130 | );
131 | const ignoreButtons = document.querySelectorAll(
132 | `.${O_POPUP_IGNORE_BUTTON}`
133 | );
134 | ignoreButtons.forEach((button) =>
135 | button.addEventListener('click', self.onIgnore)
136 | );
137 | self.reloader = document.getElementById('reloader');
138 | if (self.reloader) {
139 | self.reloader.addEventListener('click', self.onRun);
140 | }
141 | self.dictionaryOpener = document.getElementById('dictionary-opener');
142 | if (self.dictionaryOpener) {
143 | self.dictionaryOpener.addEventListener('click', self.onOpenDictionary);
144 | }
145 | self.runner = document.getElementById('runner');
146 | if (self.runner) {
147 | self.runner.addEventListener('click', self.onRun);
148 | }
149 | self.checker = document.getElementById('checker');
150 | if (self.checker) {
151 | self.checker.addEventListener('click', self.onRun);
152 | }
153 | self.sizer = document.getElementById('sizer');
154 | if (self.sizer) {
155 | self.sizer.addEventListener('click', self.onResize);
156 | }
157 | self.closer = document.getElementById('closer');
158 | if (self.closer) {
159 | self.closer.addEventListener('click', self.onClose);
160 | }
161 | self.mover = document.querySelector(`.${O_POPUP_CONTROLS}`);
162 | if (self.mover) {
163 | self.mover.addEventListener('mousedown', self.moverIsDown);
164 | }
165 | document.addEventListener('mouseup', self.onMouseUp);
166 | document.addEventListener('mousemove', self.onMouseMove);
167 | }
168 |
169 | private removeListeners() {
170 | const minicards = document.querySelectorAll(`.${O_POPUP_ITEM}`);
171 | minicards.forEach((mc) =>
172 | mc.removeEventListener('click', self.onClickByHint)
173 | );
174 | minicards.forEach((mc) =>
175 | mc.removeEventListener('mouseover', self.onFocusWord)
176 | );
177 | minicards.forEach((mc) =>
178 | mc.removeEventListener('mouseout', self.onRemoveFocusWord)
179 | );
180 | const replacements = document.querySelectorAll(
181 | `.${O_POPUP_WORD_TO_REPLACE}`
182 | );
183 | replacements.forEach((rp) =>
184 | rp.removeEventListener('click', self.onReplaceWord)
185 | );
186 | const ignoreButtons = document.querySelectorAll(
187 | `.${O_POPUP_IGNORE_BUTTON}`
188 | );
189 | ignoreButtons.forEach((button) =>
190 | button.removeEventListener('click', self.onIgnore)
191 | );
192 | if (self.reloader) self.reloader.removeEventListener('click', self.onRun);
193 | if (self.dictionaryOpener)
194 | self.dictionaryOpener.removeEventListener('click', self.onOpenDictionary);
195 | if (self.checker) self.checker.removeEventListener('click', self.onRun);
196 | if (self.runner) self.runner.removeEventListener('click', self.onRun);
197 | if (self.sizer) self.sizer.removeEventListener('click', self.onResize);
198 | if (self.closer) self.closer.removeEventListener('click', self.onClose);
199 | if (self.mover)
200 | self.mover.removeEventListener('mousedown', self.moverIsDown);
201 | document.removeEventListener('mouseup', self.onMouseUp);
202 | document.removeEventListener('mousemove', self.onMouseMove);
203 | }
204 |
205 | private onClickByHint(e: any): void {
206 | const opened = document.querySelectorAll(`.${O_POPUP_ITEM_OPENED}`);
207 | opened.forEach((o) => o.classList.remove(O_POPUP_ITEM_OPENED));
208 | if (e.currentTarget.classList.contains(O_POPUP_ITEM_OPENED)) {
209 | e.currentTarget.classList.remove(O_POPUP_ITEM_OPENED);
210 | } else {
211 | e.currentTarget.classList.add(O_POPUP_ITEM_OPENED);
212 | }
213 |
214 | const begin = e.currentTarget.dataset.begin;
215 | if (begin) {
216 | self.scrollToWord(begin);
217 | }
218 | }
219 |
220 | private moverIsDown(e: any) {
221 | self.moverSelected = true;
222 | self.popupOffset = [
223 | self.popup.offsetLeft - e.clientX,
224 | self.popup.offsetTop - e.clientY
225 | ];
226 | }
227 |
228 | private onMouseUp() {
229 | self.moverSelected = false;
230 | }
231 |
232 | private onMouseMove(e: any) {
233 | e.preventDefault();
234 | if (self.moverSelected) {
235 | const mousePosition = {
236 | x: e.clientX,
237 | y: e.clientY
238 | };
239 | self.popup.style.left = `${mousePosition.x + self.popupOffset[0]}px`;
240 | self.popup.style.top = `${mousePosition.y + self.popupOffset[1]}px`;
241 | }
242 | }
243 |
244 | private onResize() {
245 | if (self.popup.className.contains(O_POPUP_RESIZED)) {
246 | self.popup.classList.remove(O_POPUP_RESIZED);
247 | } else {
248 | self.popup.classList.add(O_POPUP_RESIZED);
249 | }
250 | }
251 |
252 | private onClose() {
253 | self.emitter.trigger('orthography:close');
254 | }
255 |
256 | private onFocusWord(e: any) {
257 | const begin = e.currentTarget.dataset.begin;
258 | const word = document.querySelector(`.begin-${begin}`);
259 | if (word) {
260 | word.classList.add(O_HIGHLIGHT_FOCUSED);
261 | }
262 | }
263 |
264 | private onRemoveFocusWord() {
265 | const words = document.querySelectorAll(`.${O_HIGHLIGHT_FOCUSED}`);
266 | words.forEach((w) => w.classList.remove(O_HIGHLIGHT_FOCUSED));
267 | }
268 |
269 | private onRun() {
270 | self.emitter.trigger('orthography:run');
271 | }
272 |
273 | private onReplaceWord(event: any) {
274 | self.emitter.trigger('orthography:replace', event);
275 | const { index } = event.currentTarget.dataset;
276 | const selectedItem = document.getElementById(`${O_POPUP_ITEM}-${index}`);
277 | if (selectedItem) selectedItem.remove();
278 | if (!document.querySelectorAll(`.${O_POPUP_ITEM}`).length) {
279 | self.removeLoader();
280 | }
281 | }
282 |
283 | private onIgnore(event: any) {
284 | self.emitter.trigger('orthography:ignore', event);
285 | const { index } = event.currentTarget.dataset;
286 | const selectedItem = document.getElementById(`${O_POPUP_ITEM}-${index}`);
287 | if (selectedItem) selectedItem.remove();
288 | if (!document.querySelectorAll(`.${O_POPUP_ITEM}`).length) {
289 | self.removeLoader();
290 | }
291 | }
292 |
293 | private onOpenDictionary() {
294 | self.update(null, false, true);
295 | }
296 |
297 | private onOpenCard(event: any) {
298 | const { value: begin } = event.currentTarget.attributes.begin;
299 | const popup: any = document.querySelector(`.${O_POPUP}`);
300 | const opened = document.querySelectorAll(`.${O_POPUP_ITEM_OPENED}`);
301 | opened.forEach((o) => o.classList.remove(O_POPUP_ITEM_OPENED));
302 | const selected: any = document.querySelector(`[data-begin="${begin}"]`);
303 | selected.classList.add(O_POPUP_ITEM_OPENED);
304 | popup.scrollTop = selected.offsetTop;
305 | }
306 |
307 | private scrollToWord(begin: number) {
308 | const activeEditor = self.getEditor();
309 | if (activeEditor) {
310 | activeEditor.scrollTo(0, +begin - 300);
311 | } else {
312 | self.onClose();
313 | new Notice(O_NOT_OPEN_FILE);
314 | }
315 | }
316 |
317 | private getEditor() {
318 | const activeLeaf: any = this.app.workspace.activeLeaf;
319 | const sourceMode = activeLeaf.view.sourceMode;
320 | if (!sourceMode) return null;
321 | return activeLeaf.view.sourceMode.cmEditor;
322 | }
323 | }
324 |
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | .obsidian-orthography-highlight {
2 | color: inherit;
3 | border-bottom: 1px solid rgba(255, 0, 0, 1);
4 | position: relative;
5 | }
6 |
7 | .obsidian-orthography-highlight--focused {
8 | background-color: rgba(255, 0, 0, 0.6) !important;
9 | }
10 |
11 | .obsidian-orthography-highlight:hover {
12 | background-color: rgba(255, 0, 0, 0.6);
13 | }
14 |
15 | .obsidian-orthography-tooltip {
16 | display: flex;
17 | flex-direction: column;
18 | position: absolute;
19 | z-index: 3;
20 | border-radius: 3px;
21 | box-shadow: 5px 5px 20px 0 rgba(0, 0, 0, 0.4);
22 | background-color: #202020;
23 | color: inherit;
24 | opacity: 0;
25 | height: 0;
26 | }
27 |
28 | .obsidian-orthography-tooltip--visible {
29 | opacity: 1;
30 | height: auto;
31 | animation: bounce 0.2s ease;
32 | }
33 |
34 | .obsidian-orthography-tooltip--visible button {
35 | margin: 2px 0;
36 | }
37 |
38 | .obsidian-orthography-runner {
39 | width: 40px;
40 | max-width: 40px;
41 | height: 40px;
42 | max-height: 40px;
43 | display: flex;
44 | align-items: center;
45 | justify-content: center;
46 | position: fixed;
47 | bottom: 40px;
48 | right: 40px;
49 | font-size: 22px;
50 | border-radius: 100%;
51 | box-shadow: 5px 5px 20px 0 rgba(0, 0, 0, 0.4);
52 | z-index: 2;
53 | transition: all 0.1s ease;
54 | color: inherit;
55 | cursor: pointer;
56 | }
57 |
58 | .obsidian-orthography-runner--clear {
59 | font-size: 25px;
60 | }
61 |
62 | .obsidian-orthography-runner--hidden {
63 | display: none !important;
64 | }
65 |
66 | .obsidian-orthography-runner--active {
67 | box-shadow: 2px 2px 5px 0 rgba(0, 0, 0, 0.4);
68 | transform: translateY(1px);
69 | }
70 |
71 | .obsidian-orthography-runner--active span {
72 | animation: spin 1s linear infinite;
73 | }
74 |
75 | .obsidian-orthography-runner--loading span {
76 | animation: spin 1s linear infinite;
77 | }
78 |
79 | @keyframes spin {
80 | 100% {
81 | transform: rotate(360deg);
82 | }
83 | }
84 |
85 | @keyframes bounce {
86 | 0% {
87 | transform: scale(1);
88 | }
89 |
90 | 50% {
91 | transform: scale(1.05);
92 | }
93 |
94 | 100% {
95 | transform: scale(1);
96 | }
97 | }
98 |
99 | .obsidian-orthography-popup {
100 | width: 400px;
101 | max-width: 400px;
102 | height: 400px;
103 | position: fixed;
104 | bottom: 100px;
105 | right: 60px;
106 | padding: 0 10px 3px;
107 | overflow: auto;
108 | background-color: inherit;
109 | box-shadow: 2px 2px 10px 0 rgba(0, 0, 0, 0.4);
110 | border-radius: 5px;
111 | z-index: 15;
112 | }
113 |
114 | .obsidian-orthography-popup--disabled {
115 | pointer-events: none;
116 | user-select: none;
117 | }
118 |
119 | .obsidian-orthography-popup-item {
120 | width: 100%;
121 | display: flex;
122 | flex-direction: column;
123 | position: relative;
124 | align-items: center;
125 | border-radius: 5px;
126 | box-shadow: 2px 2px 10px 0 rgba(0, 0, 0, 0.4);
127 | background-color: inherit;
128 | color: inherit;
129 | padding: 10px 15px;
130 | padding-right: 20px;
131 | box-sizing: border-box;
132 | margin-bottom: 10px;
133 | cursor: pointer;
134 | transition: all 0.2s ease;
135 | }
136 |
137 | .obsidian-orthography-popup--resized {
138 | width: 500px;
139 | max-width: 500px;
140 | }
141 |
142 | .obsidian-orthography-popup-item:hover {
143 | transform: scale(1.015);
144 | }
145 |
146 | .obsidian-orthography-popup-item:hover .obsidian-orthography-popup-arrows {
147 | opacity: 0.5;
148 | }
149 |
150 | .obsidian-orthography-popup-item--opened .obsidian-orthography-popup-minicard {
151 | display: none;
152 | }
153 |
154 | .obsidian-orthography-popup-item--opened .obsidian-orthography-popup-card {
155 | display: flex;
156 | }
157 |
158 | /* Mini card */
159 |
160 | .obsidian-orthography-popup-minicard {
161 | width: 100%;
162 | display: flex;
163 | align-items: center;
164 | margin-right: 8px;
165 | }
166 |
167 | .obsidian-orthography-popup-minicard > div:first-child {
168 | display: flex;
169 | align-items: center;
170 | margin-right: 10px;
171 | }
172 |
173 | .obsidian-orthography-popup-minicard > div:first-child:before {
174 | content: '';
175 | width: 5px;
176 | height: 5px;
177 | border-radius: 100%;
178 | margin-right: 10px;
179 | display: block;
180 | background-color: #fbd599;
181 | }
182 |
183 | .critical .obsidian-orthography-popup-minicard > div:first-child:before {
184 | background-color: red !important;
185 | }
186 |
187 | .obsidian-orthography-popup-item-sugg {
188 | display: flex;
189 | align-items: center;
190 | font-size: 14px;
191 | opacity: 0.4;
192 | }
193 |
194 | .obsidian-orthography-popup-item-sugg::before {
195 | content: '';
196 | width: 3px;
197 | height: 3px;
198 | background-color: #ccc;
199 | border-radius: 100%;
200 | margin-right: 10px;
201 | display: block;
202 | opacity: 0.2;
203 | }
204 |
205 | /* Arrows */
206 |
207 | .obsidian-orthography-popup-arrows {
208 | height: 100%;
209 | position: absolute;
210 | right: 10px;
211 | top: 0;
212 | opacity: 0;
213 | display: flex;
214 | flex-direction: column;
215 | }
216 |
217 | .obsidian-orthography-popup-arrows svg {
218 | fill: inherit;
219 | opacity: 0.5;
220 | width: 8px;
221 | }
222 |
223 | .obsidian-orthography-popup-arrows svg:first-child {
224 | transform: rotate(180deg);
225 | margin-top: 12px;
226 | margin-right: -8px;
227 | }
228 |
229 | .obsidian-orthography-popup-arrows svg:last-child {
230 | margin-top: 6px;
231 | }
232 |
233 | /* Card */
234 |
235 | .obsidian-orthography-popup-card {
236 | width: 100%;
237 | display: none;
238 | flex-direction: column;
239 | }
240 |
241 | .obsidian-orthography-popup-card > div:first-child {
242 | display: flex;
243 | align-items: center;
244 | margin-right: 10px;
245 | font-size: 12px;
246 | opacity: 0.8;
247 | }
248 |
249 | .obsidian-orthography-popup-card > div:first-child:before {
250 | content: '';
251 | width: 5px;
252 | height: 5px;
253 | border-radius: 100%;
254 | margin-right: 10px;
255 | display: block;
256 | background-color: #fbd599;
257 | }
258 |
259 | .obsidian-orthography-popup-card--line-through {
260 | text-decoration: line-through;
261 | text-decoration-color: red;
262 | }
263 |
264 | .obsidian-orthography-popup-card-content {
265 | display: flex;
266 | flex-wrap: wrap;
267 | align-items: center;
268 | margin-left: 15px;
269 | }
270 |
271 | .obsidian-orthography-popup-card-content span {
272 | margin-top: 10px;
273 | margin-right: 5px;
274 | }
275 |
276 | .obsidian-orthography-popup-replacement {
277 | background-color: #57a884;
278 | border-radius: 3px;
279 | padding: 2px 6px;
280 | padding-right: 10px;
281 | line-height: normal;
282 | white-space: nowrap;
283 | }
284 |
285 | .obsidian-orthography-popup-replacement::before {
286 | content: '→';
287 | color: inherit;
288 | display: inline-block;
289 | vertical-align: middle;
290 | margin-right: 5px;
291 | }
292 |
293 | .obsidian-orthography-popup-hightligh--red {
294 | background-color: red;
295 | border-radius: 3px;
296 | padding: 2px 6px;
297 | line-height: normal;
298 | text-decoration: none !important;
299 | }
300 |
301 | .obsidian-orthography-popup-card > div:nth-child(3) {
302 | font-size: 14px;
303 | margin-left: 15px;
304 | opacity: 0.5;
305 | }
306 |
307 | .critical .obsidian-orthography-popup-card > div:first-child:before {
308 | background-color: red !important;
309 | }
310 |
311 | /* Controls */
312 |
313 | .obsidian-orthography-popup-controls {
314 | position: sticky;
315 | left: 0;
316 | top: 0;
317 | display: flex;
318 | justify-content: flex-end;
319 | align-items: center;
320 | background-color: inherit;
321 | padding: 5px;
322 | margin: 0 -10px 5px;
323 | z-index: 2;
324 | }
325 |
326 | .obsidian-orthography-popup-controls .obsidian-orthography-popup-controls-item {
327 | width: 30px;
328 | opacity: 0.5;
329 | font-size: 14px;
330 | cursor: pointer !important;
331 | padding: 5px;
332 | display: flex;
333 | justify-content: center;
334 | align-items: center;
335 | transition: all 0.2s ease;
336 | }
337 |
338 | .obsidian-orthography-popup-controls
339 | .obsidian-orthography-popup-controls-item:hover {
340 | opacity: 1;
341 | }
342 |
343 | .obsidian-orthography-popup-controls
344 | .obsidian-orthography-popup-controls-item:active {
345 | transform: scale(0.9);
346 | }
347 |
348 | .obsidian-orthography-popup-controls
349 | .obsidian-orthography-popup-controls-item
350 | svg {
351 | width: 16px;
352 | height: auto;
353 | fill: inherit;
354 | opacity: 0.5;
355 | }
356 |
357 | .obsidian-orthography-popup-controls .obsidian-orthography-popup-close {
358 | font-size: 16px;
359 | }
360 |
361 | .obsidian-orthography-popup-controls .obsidian-orthography-popup-run {
362 | justify-content: flex-end;
363 | }
364 |
365 | .obsidian-orthography-popup-controls:hover {
366 | cursor: move;
367 | }
368 |
369 | .obsidian-orthography-popup-controls:active {
370 | cursor: grabbing;
371 | }
372 |
373 | .obsidian-orthography-popup-horizontalSize {
374 | margin-right: auto;
375 | margin-left: 0 !important;
376 | }
377 |
378 | /* UIHint fallback */
379 |
380 | .obsidian-orthography-hints-fallback {
381 | width: 100%;
382 | height: 100%;
383 | display: flex;
384 | flex-direction: column;
385 | justify-content: center;
386 | align-items: center;
387 | position: absolute;
388 | top: 0;
389 | left: 0;
390 | }
391 |
392 | .obsidian-orthography-hints-fallback p {
393 | opacity: 0.5;
394 | }
395 |
396 | .obsidian-orthography-hints-fallback p {
397 | opacity: 0.5;
398 | }
399 |
400 | .obsidian-orthography-popup-ignore {
401 | margin-right: 0;
402 | margin-left: auto;
403 | }
404 |
405 | .obsidian-orthography-ignore-button {
406 | cursor: pointer;
407 | }
408 |
409 | /* Loader */
410 |
411 | .obsidian-orthography-loader {
412 | position: absolute;
413 | top: 0;
414 | left: 0;
415 | width: 100%;
416 | height: 100%;
417 | display: flex;
418 | justify-content: center;
419 | align-items: center;
420 | z-index: 1;
421 | opacity: 0.5;
422 | }
423 |
424 | /* Dictionary Tab */
425 |
426 | .obsidian-orthography-dictionary-container {
427 | background-color: inherit;
428 | margin-left: 20px;
429 | }
430 |
431 | .obsidian-orthography-dictionary-title {
432 | text-align: left;
433 | font-size: 20px;
434 | font-weight: bold;
435 | margin: 0 0 15px;
436 | }
437 |
438 | .obsidian-orthography-dictionary-item {
439 | display: flex;
440 | justify-content: space-between;
441 | align-items: center;
442 | padding: 7px;
443 | flex-wrap: wrap;
444 | }
445 |
446 | .obsidian-orthography-dictionary-text {
447 | font-size: 16px;
448 | word-break: break-all;
449 | overflow-wrap: break-word;
450 | }
451 |
452 | .obsidian-orthography-dictionary-button {
453 | cursor: pointer;
454 | }
455 |
456 | .obsidian-orthography-dictionary-button-container {
457 | position: sticky;
458 | display: flex;
459 | justify-content: flex-start;
460 | background-color: inherit;
461 | top: 35px;
462 | align-items: center;
463 | z-index: 2;
464 | gap: 10px;
465 | margin: 0 0 15px;
466 | }
467 |
468 | .obsidian-orthography-dictionary-word-checkbox {
469 | margin-right: 10px;
470 | }
471 |
472 | .obsidian-orthography-dictionary-word-text {
473 | display: flex;
474 | align-items: center;
475 | cursor: pointer;
476 | }
477 |
478 | .obsidian-orthography-dictionary-empty {
479 | width: 100%;
480 | text-align: center;
481 | font-size: 14px;
482 | opacity: 0.5;
483 | position: absolute;
484 | top: 50%;
485 | left: 50%;
486 | transform: translate(-50%, -50%);
487 | }
488 |
--------------------------------------------------------------------------------