diff --git a/main.ts b/main.ts index d137091..f5bc84c 100644 --- a/main.ts +++ b/main.ts @@ -34,6 +34,7 @@ export interface BulkRenamePluginSettings { tags: string[]; regExpState: { regExp: string; + withRegExpForReplaceSymbols: boolean; flags: RegExpFlag[]; }; viewType: 'tags' | 'folder' | 'regexp'; @@ -47,6 +48,7 @@ const DEFAULT_SETTINGS: BulkRenamePluginSettings = { regExpState: { regExp: '', flags: [], + withRegExpForReplaceSymbols: false, }, tags: [], viewType: 'folder', @@ -269,20 +271,48 @@ export class BulkRenameSettingsTab extends PluginSettingTab { .controlEl.addClass('bulk_regexp_control'); } + renderUseRegExpForExistingAndReplacement() { + if (!isViewTypeRegExp(this.plugin.settings)) { + return; + } + + const newSettings2 = new Setting(this.containerEl); + + newSettings2 + .setName('Use RegExp For Existing & Replacement?') + .setDesc( + "Only RegExp will work now, however it doesn't prevent you to pass string", + ) + .addToggle((toggle) => { + toggle + .setValue( + this.plugin.settings.regExpState.withRegExpForReplaceSymbols, + ) + .setTooltip('Use RegExp For Existing & Replacement?') + .onChange((isRegExpForNames) => { + this.plugin.settings.regExpState.withRegExpForReplaceSymbols = + isRegExpForNames; + this.reRenderPreview(); + this.plugin.saveSettings(); + }); + }); + } + renderReplaceSymbol() { const { settings } = this.plugin; + this.renderUseRegExpForExistingAndReplacement(); const newSettings = new Setting(this.containerEl); - newSettings.infoEl.style.display = 'none'; - - newSettings.addText((textComponent) => { - if (Platform.isDesktop) { - const previewLabel = createPreviewElement('Existing'); - textComponent.inputEl.insertAdjacentElement( - 'beforebegin', - previewLabel, - ); - } + if (Platform.isDesktop) { + const previewLabel = createPreviewElement('Existing'); + const replacementLabel = createPreviewElement('Replacement'); + newSettings.infoEl.replaceChildren(previewLabel, replacementLabel); + newSettings.setClass('flex'); + newSettings.setClass('flex-col'); + newSettings.infoEl.addClass('bulk_info'); + } + newSettings.controlEl.addClass('replaceRenderSymbols'); + newSettings.addTextArea((textComponent) => { textComponent.setValue(settings.existingSymbol); textComponent.setPlaceholder('existing chars'); textComponent.onChange((newValue) => { @@ -293,14 +323,7 @@ export class BulkRenameSettingsTab extends PluginSettingTab { textComponent.inputEl.onblur = this.reRenderPreview; }); - newSettings.addText((textComponent) => { - if (Platform.isDesktop) { - const previewLabel = createPreviewElement('Replacement'); - textComponent.inputEl.insertAdjacentElement( - 'beforebegin', - previewLabel, - ); - } + newSettings.addTextArea((textComponent) => { textComponent.setValue(settings.replacePattern); textComponent.setPlaceholder('replace with'); textComponent.onChange((newValue) => { @@ -323,7 +346,7 @@ export class BulkRenameSettingsTab extends PluginSettingTab { text: `Total Files: ${this.plugin.settings.fileNames.length}`, }); - this.filesAndPreview.infoEl.style.display = 'none'; + this.filesAndPreview.infoEl.detach(); this.filesAndPreview.controlEl.addClass('bulk_rename_preview'); this.reRenderPreview(); diff --git a/src/components/RenderPreviewFiles.ts b/src/components/RenderPreviewFiles.ts index f06ad53..a306553 100644 --- a/src/components/RenderPreviewFiles.ts +++ b/src/components/RenderPreviewFiles.ts @@ -52,6 +52,7 @@ export const syncScrolls = ( ) => { existingFilesArea.addEventListener('scroll', (event) => { const target = event.target as HTMLTextAreaElement; + if (target.scrollTop !== state.previewScroll) { previewArea.scrollTop = target.scrollTop; state.previewScroll = target.scrollTop; diff --git a/src/services/file.service.ts b/src/services/file.service.ts index cd529fb..9d48ed9 100644 --- a/src/services/file.service.ts +++ b/src/services/file.service.ts @@ -1,4 +1,5 @@ import { App, Notice, TFile } from 'obsidian'; +import XRegExp from 'xregexp'; import BulkRenamePlugin, { BulkRenamePluginSettings } from '../../main'; import { isViewTypeFolder } from './settings.service'; import { ROOT_FOLDER_NAME } from '../constants/folders'; @@ -49,16 +50,24 @@ export const selectFilenamesWithReplacedPath = (plugin: BulkRenamePlugin) => { export const replaceFilePath = (plugin: BulkRenamePlugin, file: TFile) => { const pathWithoutExtension = file.path.split('.').slice(0, -1).join('.'); - const { replacePattern, existingSymbol } = plugin.settings; + const { replacePattern, existingSymbol, regExpState } = plugin.settings; if (isRootFilesSelected(plugin)) { const newPath = replacePattern + pathWithoutExtension; return `${newPath}.${file.extension}`; } - const convertedToRegExpString = escapeRegExp(existingSymbol); - const regExpSymbol = new RegExp(convertedToRegExpString, 'g'); - const newPath = pathWithoutExtension?.replace(regExpSymbol, replacePattern); + let regExpExistingSymbol: RegExp | string = existingSymbol; + if (regExpState.withRegExpForReplaceSymbols) { + regExpExistingSymbol = XRegExp(existingSymbol, 'x'); + } + + const newPath = XRegExp.replace( + pathWithoutExtension, + regExpExistingSymbol, + replacePattern, + 'all', + ); return `${newPath}.${file.extension}`; }; @@ -102,13 +111,6 @@ export const renameFilesInObsidian = async ( success && new Notice('successfully renamed all files'); }; -let reRegExpChar = /[\\^$.*+?()[\]{}]/g, - reHasRegExpChar = RegExp(reRegExpChar.source); - -export function escapeRegExp(s: string) { - return s && reHasRegExpChar.test(s) ? s.replace(reRegExpChar, '\\$&') : s; -} - const isRootFilesSelected = (plugin: BulkRenamePlugin) => { const { existingSymbol, folderName } = plugin.settings; diff --git a/src/services/file.services.test.ts b/src/services/file.services.test.ts index 8363bca..97546e6 100644 --- a/src/services/file.services.test.ts +++ b/src/services/file.services.test.ts @@ -28,6 +28,9 @@ describe('File Services', () => { settings: { replacePattern: '-', existingSymbol: '_', + regExpState: { + withRegExpForReplaceSymbols: false, + }, }, } as unknown as BulkRenamePlugin; @@ -48,6 +51,9 @@ describe('File Services', () => { settings: { replacePattern: '-', existingSymbol: '.', + regExpState: { + withRegExpForReplaceSymbols: false, + }, }, } as unknown as BulkRenamePlugin; @@ -68,6 +74,9 @@ describe('File Services', () => { settings: { replacePattern: 'days', existingSymbol: 'journals', + regExpState: { + withRegExpForReplaceSymbols: false, + }, }, } as unknown as BulkRenamePlugin; @@ -106,6 +115,9 @@ describe('File Services', () => { const mockPluginPlugin = { settings: { fileNames: files, + regExpState: { + withRegExpForReplaceSymbols: false, + }, }, } as unknown as BulkRenamePlugin; @@ -116,6 +128,9 @@ describe('File Services', () => { ...mockPluginPlugin.settings, existingSymbol: 'journals|pages|bulkRenameTets|canWe', replacePattern: 'qwe', + regExpState: { + withRegExpForReplaceSymbols: true, + }, }, } as unknown as BulkRenamePlugin; @@ -157,6 +172,99 @@ describe('File Services', () => { expect(files).toEqual(updatedFiles); }); + + it('should rename many files using capture groups', () => { + const plugin = { + ...mockPluginPlugin, + settings: { + ...mockPluginPlugin.settings, + existingSymbol: '2022_(.+)', + replacePattern: '$1_2022', + regExpState: { + withRegExpForReplaceSymbols: true, + }, + }, + } as unknown as BulkRenamePlugin; + + const expectedResults = [ + { + path: 'journals/10_13_2022.md', + extension: 'md', + }, + { + path: 'pages/10_13_2022.md', + extension: 'md', + }, + { + path: 'bulkRenameTets/10_13_2022.md', + extension: 'md', + }, + { + path: 'YesWecan/canWe/10_13_2022.md', + extension: 'md', + }, + ]; + + const updatedFiles = selectFilenamesWithReplacedPath(plugin); + + expect(expectedResults).toEqual(updatedFiles); + }); + + it('should rename many files using capture groups', () => { + const files = [ + { + path: '2022_10_13.md', + extension: 'md', + }, + { + path: '2022_10_14.md', + extension: 'md', + }, + { + path: '2022_10_15.md', + extension: 'md', + }, + { + path: '2022_10_16.md', + extension: 'md', + }, + ] as unknown as TFile[]; + const plugin = { + settings: { + fileNames: files, + existingSymbol: `(? [0-9]{4} ) _? # year + (? [0-9]{2} ) _? # month + (? [0-9]{2} ) # day`, + replacePattern: '$-$-$', + regExpState: { + withRegExpForReplaceSymbols: true, + }, + }, + } as unknown as BulkRenamePlugin; + + const expectedResults = [ + { + path: '10-13-2022.md', + extension: 'md', + }, + { + path: '10-14-2022.md', + extension: 'md', + }, + { + path: '10-15-2022.md', + extension: 'md', + }, + { + path: '10-16-2022.md', + extension: 'md', + }, + ]; + + const updatedFiles = selectFilenamesWithReplacedPath(plugin); + + expect(expectedResults).toEqual(updatedFiles); + }); }); }); }); diff --git a/styles.css b/styles.css index 495690a..19daf61 100644 --- a/styles.css +++ b/styles.css @@ -8,10 +8,38 @@ gap: 0; } +.flex { + display: flex; +} +.flex-col { + flex-direction: column; +} + +.m-auto { + margin: auto; +} + +.bulk_info { + display: flex; + justify-content: space-between; + margin: auto; + width: 100%; +} + .bulk_rename_preview > textarea { height: 360px; } +.replaceRenderSymbols { + display: flex; + width: 100%; + padding-top: 1rem; +} + +.setting-item-control .bulk_preview_textarea { + min-width: 19em; +} + .bulk_preview_textarea { margin-left: 5px; margin-right: 5px; @@ -19,9 +47,6 @@ width: 100%; height: 400px; resize: none; - width: 100%; - /*white-space: nowrap;*/ - /*overflow: auto;*/ } .bulk_button { @@ -33,12 +58,18 @@ margin-bottom: 5px; } -.bulk_input { +.setting-item-control .bulk_input { width: 100%; + resize: none; + min-width: auto; + min-height: auto; +} + +.setting-item-control .bulk_input:first-child { margin-right: 15px; } -.bulk_preview_label { +.bulk_preview_label:first-child { margin-right: 15px; }