From e7d7ab8330e7afad6f55be29daa3345f055b07d6 Mon Sep 17 00:00:00 2001 From: Jacob Levernier Date: Sat, 22 Jan 2022 14:02:49 -0500 Subject: [PATCH] 2.0.0 (#39) - Added commands to use the clipboard. - Added inline Regex validation in Settings page. --- README.md | 4 +- manifest.json | 2 +- package.json | 2 +- src/ApplyPattern.ts | 82 ++++++++++--- src/Settings.guard.ts | 4 +- src/Settings.ts | 24 ++-- src/SettingsTab.ts | 268 +++++++++++++++++++++++++++++++++++++----- src/main.ts | 58 ++++----- styles.css | 12 ++ versions.json | 3 +- 10 files changed, 367 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 2c31507..fe3f521 100644 --- a/README.md +++ b/README.md @@ -91,10 +91,12 @@ Follow the steps below to install the plugin. - This plugin uses the [ECMAScript / Javascript flavor](https://www.regular-expressions.info/javascript.html) of Regular Expressions. - Within a Pattern, rules execute sequentially. Thus, the output of Rule 1 is passed as input to Rule 2, and the output of Rule 2 is passed as input to Rule 3, etc. At the end of the set of rules, the final output is used to replace the text in the editor. -- The plugin provides three commands by default: +- The plugin provides five commands by default: - `Apply Patterns: Apply pattern to whole lines` will loop over each line that is selected in the editor, and apply the Pattern to the entirety of each line. - - `Apply Patterns: Apply pattern to whole document` will apply the Pattern to the entire document, as one (potentially multi-line) string. - `Apply Patterns: Apply pattern to selection` will apply the Pattern to just the text selected in the editor, as one (potentially multi-line) string. + - `Apply Patterns: Apply pattern to whole clipboard` will apply the Pattern as with "`Apply pattern to whole document`" to the clipboard. + - `Apply Patterns: Apply pattern to clipboard (line-by-line)` will apply the Pattern as with "`Apply pattern to whole lines`" to the clipboard. - In addition, you can set additional commands in the Settings tab. - Within the Settings tab: - Each rule can be disabled, moved up, and moved down in the pattern. diff --git a/manifest.json b/manifest.json index 354f396..b5f1c4a 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-apply-patterns", "name": "Apply Patterns", - "version": "1.4.1", + "version": "2.0.0", "minAppVersion": "0.13.9", "description": "Apply custom patterns of find-and-replace in succession to text.", "author": "Jacob Levernier", diff --git a/package.json b/package.json index 2a79467..9ee82a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-apply-patterns", - "version": "1.4.1", + "version": "2.0.0", "description": "An Obsidian plugin for applying patterns of find and replace in succession.", "main": "main.js", "scripts": { diff --git a/src/ApplyPattern.ts b/src/ApplyPattern.ts index 514f7ff..298f2e6 100644 --- a/src/ApplyPattern.ts +++ b/src/ApplyPattern.ts @@ -11,7 +11,6 @@ import { import { validateRuleString } from './ValidateRuleString'; import { PatternModal } from './PatternsModal'; import { Command, getSettings } from './Settings'; -import { cursorTo } from 'readline'; const calculateCursorPoints = ( minLine: number, @@ -74,21 +73,18 @@ const calculateCursorPoints = ( }; export const applyPattern = ( - checking: boolean, editor: Editor, view: View, app: App, - mode: 'lines' | 'selection' | 'document' = 'lines', + mode: + | 'lines' + | 'selection' + | 'document' + | 'clipboard' + | 'clipboardLines' = 'lines', command?: Command, ) => { - if (checking) { - // editorCallback always happens in a MarkdownView; the command should - // only be shown in MarkdownView: - return true; - } - if (!(view instanceof MarkdownView)) { - // Should never happen due to check above. return; } @@ -100,10 +96,6 @@ export const applyPattern = ( const onChooseItem = (patternIndex: number): void => { const pattern = getSettings().patterns[patternIndex]; - const cursorFrom = editor.getCursor('from'); - const cursorTo = editor.getCursor('to'); - const minLine = cursorFrom.line; - const maxLine = cursorTo.line; // Confirm that each rule's strings are valid: let allValid = true; @@ -167,6 +159,68 @@ export const applyPattern = ( return; // Stop the function prematurely } + if (mode === 'clipboard' || mode === 'clipboardLines') { + // This is largely the same as the 'document' mode code, but using + // the clipboard as input. + navigator.clipboard.readText().then((clipboardText) => { + if (mode === 'clipboard') { + pattern.rules.forEach((rule, ruleIndex) => { + clipboardText = clipboardText.replace( + new RegExp( + allRuleStringsValidated[ruleIndex].from, + `u${rule.caseInsensitive ? 'i' : ''}${ + rule.global ? 'g' : '' + }${rule.multiline ? 'm' : ''}${ + rule.sticky ? 's' : '' + }`, + ), + allRuleStringsValidated[ruleIndex].to, + ); + }); + } + if (mode === 'clipboardLines') { + const clipboardTextSplit = clipboardText.split('\n'); + const updatedLines: string[] = []; + for ( + let lineNumber = 0; + lineNumber < clipboardTextSplit.length; + lineNumber++ + ) { + let line = clipboardTextSplit[lineNumber]; + pattern.rules.forEach((rule, ruleIndex) => { + if (rule.disabled === true) { + // Skip the rule if it's disabled: + return; + } + line = line.replace( + new RegExp( + allRuleStringsValidated[ruleIndex].from, + `u${rule.caseInsensitive ? 'i' : ''}${ + rule.global ? 'g' : '' + }${rule.multiline ? 'm' : ''}${ + rule.sticky ? 's' : '' + }`, + ), + allRuleStringsValidated[ruleIndex].to, + ); + }); + updatedLines.push(line); + } + clipboardText = updatedLines.join('\n'); + } + + navigator.clipboard.writeText(clipboardText); + + new Notice('Clipboard updated.'); + }); + return; + } + + const cursorFrom = editor.getCursor('from'); + const cursorTo = editor.getCursor('to'); + const minLine = cursorFrom.line; + const maxLine = cursorTo.line; + const transaction: EditorTransaction = { changes: [], }; diff --git a/src/Settings.guard.ts b/src/Settings.guard.ts index 24c8e6c..8874431 100644 --- a/src/Settings.guard.ts +++ b/src/Settings.guard.ts @@ -65,6 +65,8 @@ export function isCommand(obj: any, _argumentName?: string): obj is Command { typeof obj.patternFilter === "string" && typeof obj.selection === "boolean" && typeof obj.lines === "boolean" && - typeof obj.document === "boolean" + typeof obj.document === "boolean" && + typeof obj.clipboard === "boolean" && + typeof obj.clipboardLines === "boolean" ) } diff --git a/src/Settings.ts b/src/Settings.ts index fcc895c..d5e518e 100644 --- a/src/Settings.ts +++ b/src/Settings.ts @@ -68,19 +68,23 @@ export const defaultSettings: Settings = { /** @see {isCommand} ts-auto-guard:type-guard */ export interface Command { - name: string; - patternFilter: string; - selection?: boolean; - lines?: boolean; - document?: boolean; + name: string; + patternFilter: string; + selection?: boolean; + lines?: boolean; + document?: boolean; + clipboard?: boolean; + clipboardLines?: boolean; } export const defaultCommandSettings: Command = { - name: '', - patternFilter: '', - selection: true, - lines: true, - document: true, + name: '', + patternFilter: '', + selection: true, + lines: true, + document: true, + clipboard: false, + clipboardLines: false, }; export const formatUnnamedPattern = (patternIndex: number): string => diff --git a/src/SettingsTab.ts b/src/SettingsTab.ts index f23d95f..c39c43e 100644 --- a/src/SettingsTab.ts +++ b/src/SettingsTab.ts @@ -140,7 +140,22 @@ export class SettingsTab extends PluginSettingTab { }); patternsDefaultsEl.createEl('h3', { text: `Pattern defaults` }); - new Setting(patternsDefaultsEl) + const patternsDefaultStartEl = patternsDefaultsEl.createEl('div'); + const patternsDefaultStartSetting = new Setting(patternsDefaultStartEl); + const patternsDefaultStartValidEl = + patternsDefaultStartEl.createEl('span'); + patternsDefaultStartValidEl.addClass('validation-text'); + const patternsDefaultStartValid = validateRuleString( + getSettings().defaultCursorRegexStart || '', + ); + if (patternsDefaultStartValid.valid !== true) { + patternsDefaultStartEl.addClass('invalid'); + patternsDefaultStartValidEl.setText( + patternsDefaultStartValid.string, + ); + } + + patternsDefaultStartSetting .setName('Post-pattern cursor/selection start (Regex)') .setDesc( 'A regular expression to determine the default starting location of the cursor after a Pattern has been applied. The cursor will be placed at the ending location of the first match.', @@ -155,11 +170,34 @@ export class SettingsTab extends PluginSettingTab { }); await this.plugin.saveSettings(); + + const valueValid = validateRuleString(value); + if (valueValid.valid === true) { + patternsDefaultStartEl.removeClass('invalid'); + patternsDefaultStartValidEl.setText(''); + } else { + patternsDefaultStartEl.addClass('invalid'); + patternsDefaultStartValidEl.setText( + valueValid.string, + ); + } }, ); }); - new Setting(patternsDefaultsEl) + const patternsDefaultEndEl = patternsDefaultsEl.createEl('div'); + const patternsDefaultEndSetting = new Setting(patternsDefaultEndEl); + const patternsDefaultEndValidEl = patternsDefaultEndEl.createEl('span'); + patternsDefaultEndValidEl.addClass('validation-text'); + const patternsDefaultEndValid = validateRuleString( + getSettings().defaultCursorRegexEnd || '', + ); + if (patternsDefaultEndValid.valid !== true) { + patternsDefaultEndEl.addClass('invalid'); + patternsDefaultEndValidEl.setText(patternsDefaultEndValid.string); + } + + patternsDefaultEndSetting .setName('Post-pattern cursor/selection end (Regex)') .setDesc( 'A regular expression to determine the default ending location of the cursor after the Pattern has been applied. The cursor will be placed at the ending location of the first match.', @@ -174,6 +212,17 @@ export class SettingsTab extends PluginSettingTab { }); await this.plugin.saveSettings(); + + const valueValid = validateRuleString(value); + if (valueValid.valid === true) { + patternsDefaultEndEl.removeClass('invalid'); + patternsDefaultEndValidEl.setText(''); + } else { + patternsDefaultEndEl.addClass('invalid'); + patternsDefaultEndValidEl.setText( + valueValid.string, + ); + } }, ); }); @@ -528,7 +577,17 @@ export class SettingsTab extends PluginSettingTab { }); }); - new Setting(ruleEl) + const ruleFromEl = ruleEl.createEl('div'); + const ruleFromElSetting = new Setting(ruleFromEl); + const ruleFromValidEl = ruleFromEl.createEl('span'); + ruleFromValidEl.addClass('validation-text'); + const ruleFromValid = validateRuleString(rule.from); + if (ruleFromValid.valid !== true) { + ruleFromEl.addClass('invalid'); + ruleFromValidEl.setText(ruleFromValid.string); + } + + ruleFromElSetting .setName('Matching text (Regex)') .addText((text) => { text.setPlaceholder('') @@ -552,6 +611,15 @@ export class SettingsTab extends PluginSettingTab { }); await this.plugin.saveSettings(); + + const valueValid = validateRuleString(value); + if (valueValid.valid === true) { + ruleFromEl.removeClass('invalid'); + ruleFromValidEl.setText(''); + } else { + ruleFromEl.addClass('invalid'); + ruleFromValidEl.setText(valueValid.string); + } }); }); @@ -642,32 +710,51 @@ export class SettingsTab extends PluginSettingTab { }); }); - new Setting(ruleEl) - .setName('Replacement text') - .addText((text) => { - text.setPlaceholder('') - .setValue(rule.to) - .onChange(async (value) => { - const newPatterns = cloneDeep( - getSettings().patterns, - ); - newPatterns[patternIndex].rules.splice( - ruleIndex, - 1, - { - ...newPatterns[patternIndex].rules[ - ruleIndex - ], - to: value || '', - }, - ); - updateSettings({ - patterns: newPatterns, - }); + const ruleToEl = ruleEl.createEl('div'); + const ruleToElSetting = new Setting(ruleToEl); + const ruleToValidEl = ruleFromEl.createEl('span'); + ruleToValidEl.addClass('validation-text'); + const ruleToValid = validateRuleString(rule.to, false); - await this.plugin.saveSettings(); + if (ruleToValid.valid !== null) { + ruleToEl.addClass('invalid'); + ruleToValidEl.setText(ruleToValid.string); + } + + ruleToElSetting.setName('Replacement text').addText((text) => { + text.setPlaceholder('') + .setValue(rule.to) + .onChange(async (value) => { + const newPatterns = cloneDeep( + getSettings().patterns, + ); + newPatterns[patternIndex].rules.splice( + ruleIndex, + 1, + { + ...newPatterns[patternIndex].rules[ + ruleIndex + ], + to: value || '', + }, + ); + updateSettings({ + patterns: newPatterns, }); - }); + + await this.plugin.saveSettings(); + + const valueValid = validateRuleString(value, false); + + if (valueValid.valid === null) { + ruleToEl.removeClass('invalid'); + ruleToValidEl.setText(''); + } else { + ruleToEl.addClass('invalid'); + ruleToValidEl.setText(valueValid.string); + } + }); + }); let deleteRulePrimed = false; let ruleDeletePrimerTimer: ReturnType | null; @@ -838,7 +925,22 @@ export class SettingsTab extends PluginSettingTab { const patternCursorEl = patternEl.createEl('div'); patternCursorEl.addClass('pattern-cursor'); - new Setting(patternCursorEl) + const patternCursorStartEl = patternCursorEl.createEl('div'); + const patternCursorStartSetting = new Setting(patternCursorStartEl); + const patternCursorStartValidEl = + patternCursorStartEl.createEl('span'); + patternCursorStartValidEl.addClass('validation-text'); + const patternCursorStartValid = validateRuleString( + pattern.cursorRegexStart, + ); + if (patternCursorStartValid.valid !== true) { + patternCursorStartEl.addClass('invalid'); + patternCursorStartValidEl.setText( + patternCursorStartValid.string, + ); + } + + patternCursorStartSetting .setName('Cursor/selection start (Regex)') .setDesc( 'A regular expression to determine the starting location of the cursor after the Pattern has been applied. The cursor will be placed at the ending location of the first match.', @@ -859,10 +961,33 @@ export class SettingsTab extends PluginSettingTab { }); await this.plugin.saveSettings(); + + const valueValid = validateRuleString(value); + if (valueValid.valid === true) { + patternCursorStartEl.removeClass('invalid'); + patternCursorStartValidEl.setText(''); + } else { + patternCursorStartEl.addClass('invalid'); + patternCursorStartValidEl.setText( + valueValid.string, + ); + } }); }); - new Setting(patternCursorEl) + const patternCursorEndEl = patternCursorEl.createEl('div'); + const patternCursorEndSetting = new Setting(patternCursorEndEl); + const patternCursorEndValidEl = patternCursorEndEl.createEl('span'); + patternCursorEndValidEl.addClass('validation-text'); + const patternCursorEndValid = validateRuleString( + pattern.cursorRegexEnd, + ); + if (patternCursorEndValid.valid !== true) { + patternCursorEndEl.addClass('invalid'); + patternCursorEndValidEl.setText(patternCursorEndValid.string); + } + + patternCursorEndSetting .setName('Cursor/selection end (Regex)') .setDesc( 'A regular expression to determine the ending location of the cursor after the Pattern has been applied. The cursor will be placed at the ending location of the first match.', @@ -883,6 +1008,17 @@ export class SettingsTab extends PluginSettingTab { }); await this.plugin.saveSettings(); + + const valueValid = validateRuleString(value); + if (valueValid.valid === true) { + patternCursorEndEl.removeClass('invalid'); + patternCursorEndValidEl.setText(''); + } else { + patternCursorEndEl.addClass('invalid'); + patternCursorEndValidEl.setText( + valueValid.string, + ); + } }); }); } @@ -1116,7 +1252,23 @@ export class SettingsTab extends PluginSettingTab { }); }); - new Setting(commandEl) + const commandPatternNameEl = commandEl.createEl('div'); + const commandPatternNameSetting = new Setting(commandPatternNameEl); + const commandPatternNameValidEl = + commandPatternNameEl.createEl('span'); + commandPatternNameValidEl.addClass('validation-text'); + const commandPatternNameValid = validateRuleString( + command.patternFilter, + ); + + if (commandPatternNameValid.valid !== true) { + commandPatternNameEl.addClass('invalid'); + commandPatternNameValidEl.setText( + commandPatternNameValid.string, + ); + } + + commandPatternNameSetting .setName('Pattern name filter') .addText((text) => { text.setPlaceholder('') @@ -1134,6 +1286,18 @@ export class SettingsTab extends PluginSettingTab { }); await this.plugin.saveSettings(); + + const valueValid = validateRuleString(value); + + if (valueValid.valid === true) { + commandPatternNameEl.removeClass('invalid'); + commandPatternNameValidEl.setText(''); + } else { + commandPatternNameEl.addClass('invalid'); + commandPatternNameValidEl.setText( + valueValid.string, + ); + } }); }); @@ -1189,6 +1353,50 @@ export class SettingsTab extends PluginSettingTab { commands: newCommands, }); + await this.plugin.saveSettings(); + }); + }); + + new Setting(commandEl) + .setName('Apply to whole clipboard') + .setDesc( + 'Apply the Pattern as with "Apply to whole document" to the clipboard.', + ) + .addToggle((toggle) => { + toggle + .setTooltip('Apply to whole whole clipboard') + .setValue(command.clipboard || false) + .onChange(async (value) => { + const newCommands = cloneDeep( + getSettings().commands, + ); + newCommands[commandIndex].clipboard = value; + updateSettings({ + commands: newCommands, + }); + + await this.plugin.saveSettings(); + }); + }); + + new Setting(commandEl) + .setName('Apply to clipboard (line-by-line)') + .setDesc( + 'Apply the Pattern as with "Apply to whole lines" to the clipboard.', + ) + .addToggle((toggle) => { + toggle + .setTooltip('Apply to whole whole clipboard') + .setValue(command.clipboardLines || false) + .onChange(async (value) => { + const newCommands = cloneDeep( + getSettings().commands, + ); + newCommands[commandIndex].clipboardLines = value; + updateSettings({ + commands: newCommands, + }); + await this.plugin.saveSettings(); }); }); diff --git a/src/main.ts b/src/main.ts index a9bb775..1a8b797 100644 --- a/src/main.ts +++ b/src/main.ts @@ -25,48 +25,40 @@ export default class ApplyPatternsPlugin extends Plugin { this.addCommand({ id: 'apply-pattern-to-lines', name: 'Apply pattern to whole lines', - editorCheckCallback: ( - checking: boolean, - editor: Editor, - view: View, - ) => { - return applyPattern(checking, editor, view, this.app, 'lines'); + editorCallback: (editor: Editor, view: View) => { + return applyPattern(editor, view, this.app, 'lines'); }, }); this.addCommand({ id: 'apply-pattern-to-selection', name: 'Apply pattern to selection', - editorCheckCallback: ( - checking: boolean, - editor: Editor, - view: View, - ) => { - return applyPattern( - checking, - editor, - view, - this.app, - 'selection', - ); + editorCallback: (editor: Editor, view: View) => { + return applyPattern(editor, view, this.app, 'selection'); }, }); this.addCommand({ id: 'apply-pattern-to-document', name: 'Apply pattern to whole document', - editorCheckCallback: ( - checking: boolean, - editor: Editor, - view: View, - ) => { - return applyPattern( - checking, - editor, - view, - this.app, - 'document', - ); + editorCallback: (editor: Editor, view: View) => { + return applyPattern(editor, view, this.app, 'document'); + }, + }); + + this.addCommand({ + id: 'apply-pattern-to-clipboard-document', + name: 'Apply pattern to whole clipboard', + editorCallback: (editor: Editor, view: View) => { + return applyPattern(editor, view, this.app, 'clipboard'); + }, + }); + + this.addCommand({ + id: 'apply-pattern-to-clipboard-lines', + name: 'Apply pattern to clipboard (line-by-line)', + editorCallback: (editor: Editor, view: View) => { + return applyPattern(editor, view, this.app, 'clipboardLines'); }, }); @@ -77,6 +69,8 @@ export default class ApplyPatternsPlugin extends Plugin { selection: 'selection', lines: 'whole lines', document: 'whole document', + clipboard: 'whole clipboard', + clipboardLines: 'clipboard (line-by-line)', })) { // Get TypeScript to understand type as a key, rather than // as a string. See https://stackoverflow.com/a/62438434 @@ -89,13 +83,11 @@ export default class ApplyPatternsPlugin extends Plugin { command.name || 'Unnamed command ' + commandIndex } on ${plainLanguage}`, - editorCheckCallback: ( - checking: boolean, + editorCallback: async ( editor: Editor, view: View, ) => { return applyPattern( - checking, editor, view, this.app, diff --git a/styles.css b/styles.css index 61b3546..27eb70a 100644 --- a/styles.css +++ b/styles.css @@ -48,3 +48,15 @@ color: var(--text-error); font-size: 1.15em; } + +.invalid input[type='text'] { + background: rgb(255, 0, 0, 0.2) !important; +} + +.validation-text { + color: var(--text-error) !important; + text-align: right; + display: block; + font-size: small; + font-style: italic; +} diff --git a/versions.json b/versions.json index 76aa60a..fc5916f 100644 --- a/versions.json +++ b/versions.json @@ -7,5 +7,6 @@ "1.3.0": "0.12.0", "1.3.1": "0.12.0", "1.4.0": "0.13.9", - "1.4.1": "0.13.9" + "1.4.1": "0.13.9", + "2.0.0": "0.13.9" }