├── .eslintignore ├── .gitattributes ├── res └── dialog.png ├── versions.json ├── .gitignore ├── manifest.json ├── tsconfig.json ├── .eslintrc ├── rollup.config.js ├── package.json ├── styles.css ├── LICENSE ├── README.md └── src └── main.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | npm node_modules 2 | build 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /res/dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gru80/obsidian-regex-replace/HEAD/res/dialog.png -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.9.12", 3 | "1.1.0": "0.12.17", 4 | "1.2.0": "0.12.17" 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # npm 2 | node_modules 3 | package-lock.json 4 | *.js 5 | 6 | # vscode 7 | .vscode 8 | 9 | # Intellij 10 | *.iml 11 | .idea 12 | 13 | # Exclude sourcemaps 14 | *.map 15 | 16 | # Obsidian saved settings 17 | data.json 18 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Martin Eder", 3 | "authorUrl": "https://www.gruware.org", 4 | "description": "Find and replace text using regular expressions.", 5 | "id": "obsidian-regex-replace", 6 | "isDesktopOnly": false, 7 | "minAppVersion": "0.12.17", 8 | "name": "Regex Find/Replace", 9 | "version": "1.2.0" 10 | } 11 | -------------------------------------------------------------------------------- /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 | "lib": [ 13 | "DOM", 14 | "ES5", 15 | "ES6" 16 | ] 17 | }, 18 | "include": [ 19 | "src/**/*.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "parserOptions": { 13 | "sourceType": "module" 14 | }, 15 | "rules": { 16 | "no-unused-vars": "off", 17 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 18 | "@typescript-eslint/ban-ts-comment": "off", 19 | "no-prototype-builtins": "off", 20 | "@typescript-eslint/no-empty-function": "off" 21 | } 22 | } -------------------------------------------------------------------------------- /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 | const isProd = process.env.BUILD === "production"; 6 | 7 | const banner = `/* 8 | THIS IS A GENERATED/BUNDLED FILE BY ROLLUP 9 | if you want to view the source visit the plugins github repository 10 | */ 11 | `; 12 | 13 | export default { 14 | input: "src/main.ts", 15 | output: { 16 | dir: ".", 17 | sourcemap: "inline", 18 | sourcemapExcludeSources: isProd, 19 | format: "cjs", 20 | exports: "default", 21 | banner, 22 | }, 23 | external: ["obsidian"], 24 | plugins: [typescript(), nodeResolve({ browser: true }), commonjs()], 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-regex-replace", 3 | "version": "1.2.0", 4 | "description": "An Obsidian plugin, which allows to replace text in the current editor using regular expressions.", 5 | "main": "build/main.js", 6 | "scripts": { 7 | "dev": "rollup --config rollup.config.js -w", 8 | "build": "rollup --config rollup.config.js --environment BUILD:production" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "@rollup/plugin-commonjs": "^18.0.0", 15 | "@rollup/plugin-node-resolve": "^11.2.1", 16 | "@rollup/plugin-typescript": "^8.2.1", 17 | "@types/node": "^14.14.37", 18 | "@typescript-eslint/eslint-plugin": "^5.5.0", 19 | "obsidian": "^0.12.17", 20 | "rollup": "^2.32.1", 21 | "tslib": "^2.2.0", 22 | "typescript": "^4.2.4" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .find-replace-modal .modal-content .row { 2 | display: flex; 3 | align-items: center; 4 | width: 100%; 5 | margin-top: 8px; 6 | } 7 | 8 | 9 | .find-replace-modal .modal-content .row .input-label { 10 | display: flex; 11 | justify-content: flex-end; 12 | height: 100%; 13 | } 14 | 15 | 16 | .find-replace-modal .modal-content .row .postfix-label { 17 | display: flex; 18 | height: 100%; 19 | margin-left: 8px; 20 | justify-content: flex-end; 21 | color: gray; 22 | } 23 | 24 | 25 | .find-replace-modal .modal-content .row .check-label { 26 | display: flex; 27 | justify-content: flex-end; 28 | width: 100%; 29 | height: 100%; 30 | margin-right: 8px; 31 | } 32 | 33 | 34 | .find-replace-modal .modal-content .row .input-wrapper { 35 | display: flex; 36 | align-items: center; 37 | height: 100%; 38 | width: 100%; 39 | margin: 0; 40 | } 41 | 42 | 43 | .find-replace-modal .modal-content .row .input-wrapper input { 44 | width: 100%; 45 | } 46 | 47 | 48 | .find-replace-modal .modal-content .button-wrapper { 49 | justify-content: center; 50 | margin-top: 16px; 51 | } 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2021 Martin Eder 2 | 3 | Permission is hereby granted, free 4 | of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![release](https://img.shields.io/github/v/release/Gru80/obsidian-regex-replace) 2 | ![downloads](https://img.shields.io/github/downloads/Gru80/obsidian-regex-replace/total.svg) 3 | 4 | # Obsidian Plugin - Regex Find/Replace 5 | Provides a dialog to find and replace text in the currently opened note. 6 | In addition to Obsidians on-board find/repace function, this plugin provides options to 7 | - use regular expressions or just plain text 8 | - replace found occurances in the currently slected text or in the whole document 9 | 10 | Desktop as well as mobile versions of Obsidian are supported. 11 | 12 | ![Regex FindReplace Dialog](res/dialog.png) 13 | 14 | ## How to use 15 | - Run `Regex Find/Replace: Find and Replace using regular expressions` from the command palette or 16 | - Assign a shortcut key to this command and use it to open the dialog 17 | - The plugin will remember the last recent search/replace terms as well as the settings 18 | 19 | ## How to install 20 | ### From inside Obsidian 21 | This plugin can be installed via the `Community Plugins` tab in the Obsidian Settings dialog: 22 | - Disable Safe Mode (to enable community plugins to be installed) 23 | - Browse the community plugins searching for "regex find/replace" 24 | - Install the Plugin 25 | - Enable the plugin after installation 26 | 27 | ### Manual installation 28 | The plugin can also be installed manually from the repository: 29 | - Create a new directory in your vaults plugins directory, e.g. 30 | `.obsidian/plugins/obsidian-regex-replace` 31 | 32 | - Head over to https://github.com/Gru80/obsidian-regex-replace/releases 33 | 34 | - From the latest release, download the files 35 | - main.js 36 | - manifest.json 37 | - styles.css 38 | 39 | to your newly created plugin directory 40 | - Launch Obsidian and open the Settings dialog 41 | - Disable Safe Mode in the `Community Plugins` tab (this enables community plugins to be enabled) 42 | - Enable the new plugin 43 | 44 | ## Version History 45 | ### 1.0.0 46 | Initial release 47 | 48 | ### 1.1.0 49 | - Case insensitive search can now be enabled in the settings panel of the plugin (regex flag /i) 50 | - Find-in-selection toggle switch is disabled if no text is selected in the note 51 | - Performance improvements and bug-fixes 52 | 53 | ### 1.2.0 54 | - Option to interpret `\n` in repleace field to insert line-break accordingly 55 | - Option to pre-fill the find-field with the selected word or phrase 56 | - Used regex-modifier flags are shown in the dialog 57 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | ButtonComponent, 4 | Editor, 5 | Modal, 6 | Notice, 7 | Plugin, 8 | TextComponent, 9 | ToggleComponent, 10 | PluginSettingTab, 11 | Setting 12 | } from 'obsidian'; 13 | 14 | interface RfrPluginSettings { 15 | findText: string; 16 | replaceText: string; 17 | useRegEx: boolean; 18 | selOnly: boolean; 19 | caseInsensitive: boolean; 20 | processLineBreak: boolean; 21 | processTab: boolean; 22 | prefillFind: boolean; 23 | } 24 | 25 | const DEFAULT_SETTINGS: RfrPluginSettings = { 26 | findText: '', 27 | replaceText: '', 28 | useRegEx: true, 29 | selOnly: false, 30 | caseInsensitive: false, 31 | processLineBreak: false, 32 | processTab: false, 33 | prefillFind: false 34 | } 35 | 36 | // logThreshold: 0 ... only error messages 37 | // 9 ... verbose output 38 | const logThreshold = 9; 39 | const logger = (logString: string, logLevel=0): void => {if (logLevel <= logThreshold) console.log ('RegexFiRe: ' + logString)}; 40 | 41 | export default class RegexFindReplacePlugin extends Plugin { 42 | settings: RfrPluginSettings; 43 | 44 | async onload() { 45 | logger('Loading Plugin...', 9); 46 | await this.loadSettings(); 47 | 48 | this.addSettingTab(new RegexFindReplaceSettingTab(this.app, this)); 49 | 50 | 51 | this.addCommand({ 52 | id: 'obsidian-regex-replace', 53 | name: 'Find and Replace using regular expressions', 54 | editorCallback: (editor) => { 55 | new FindAndReplaceModal(this.app, editor, this.settings, this).open(); 56 | }, 57 | }); 58 | } 59 | 60 | onunload() { 61 | logger('Bye!', 9); 62 | } 63 | 64 | async loadSettings() { 65 | logger('Loading Settings...', 6); 66 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); 67 | logger(' findVal: ' + this.settings.findText, 6); 68 | logger(' replaceText: ' + this.settings.replaceText, 6); 69 | logger(' caseInsensitive: ' + this.settings.caseInsensitive, 6); 70 | logger(' processLineBreak: ' + this.settings.processLineBreak, 6); 71 | 72 | } 73 | 74 | async saveSettings() { 75 | await this.saveData(this.settings); 76 | } 77 | 78 | } 79 | 80 | class FindAndReplaceModal extends Modal { 81 | constructor(app: App, editor: Editor, settings: RfrPluginSettings, plugin: Plugin) { 82 | super(app); 83 | this.editor = editor; 84 | this.settings = settings; 85 | this.plugin = plugin; 86 | } 87 | 88 | settings: RfrPluginSettings; 89 | editor: Editor; 90 | plugin: Plugin; 91 | 92 | onOpen() { 93 | const { contentEl, titleEl, editor, modalEl } = this; 94 | 95 | modalEl.addClass('find-replace-modal'); 96 | titleEl.setText('Regex Find/Replace'); 97 | 98 | const rowClass = 'row'; 99 | const divClass = 'div'; 100 | const noSelection = editor.getSelection() === ''; 101 | let regexFlags = 'gm'; 102 | if (this.settings.caseInsensitive) regexFlags = regexFlags.concat('i'); 103 | 104 | logger('No text selected?: ' + noSelection, 9); 105 | 106 | const addTextComponent = (label: string, placeholder: string, postfix=''): [TextComponent, HTMLDivElement] => { 107 | const containerEl = document.createElement(divClass); 108 | containerEl.addClass(rowClass); 109 | 110 | const targetEl = document.createElement(divClass); 111 | targetEl.addClass('input-wrapper'); 112 | 113 | const labelEl = document.createElement(divClass); 114 | labelEl.addClass('input-label'); 115 | labelEl.setText(label); 116 | 117 | const labelEl2 = document.createElement(divClass); 118 | labelEl2.addClass('postfix-label'); 119 | labelEl2.setText(postfix); 120 | 121 | containerEl.appendChild(labelEl); 122 | containerEl.appendChild(targetEl); 123 | containerEl.appendChild(labelEl2); 124 | 125 | const component = new TextComponent(targetEl); 126 | component.setPlaceholder(placeholder); 127 | 128 | contentEl.append(containerEl); 129 | return [component, labelEl2]; 130 | }; 131 | 132 | const addToggleComponent = (label: string, tooltip: string, hide = false): ToggleComponent => { 133 | const containerEl = document.createElement(divClass); 134 | containerEl.addClass(rowClass); 135 | 136 | const targetEl = document.createElement(divClass); 137 | targetEl.addClass(rowClass); 138 | 139 | const component = new ToggleComponent(targetEl); 140 | component.setTooltip(tooltip); 141 | 142 | const labelEl = document.createElement(divClass); 143 | labelEl.addClass('check-label'); 144 | labelEl.setText(label); 145 | 146 | containerEl.appendChild(labelEl); 147 | containerEl.appendChild(targetEl); 148 | if (!hide) contentEl.appendChild(containerEl); 149 | return component; 150 | }; 151 | 152 | // Create input fields 153 | const findRow = addTextComponent('Find:', 'e.g. (.*)', '/' + regexFlags); 154 | const findInputComponent = findRow[0]; 155 | const findRegexFlags = findRow[1]; 156 | const replaceRow = addTextComponent('Replace:', 'e.g. $1', this.settings.processLineBreak ? '\\n=LF' : ''); 157 | const replaceWithInputComponent = replaceRow[0]; 158 | 159 | // Create and show regular expression toggle switch 160 | const regToggleComponent = addToggleComponent('Use regular expressions', 'If enabled, regular expressions in the find field are processed as such, and regex groups might be addressed in the replace field'); 161 | 162 | // Update regex-flags label if regular expressions are enabled or disabled 163 | regToggleComponent.onChange( regNew => { 164 | if (regNew) { 165 | findRegexFlags.setText('/' + regexFlags); 166 | } 167 | else { 168 | findRegexFlags.setText(''); 169 | } 170 | }) 171 | 172 | // Create and show selection toggle switch only if any text is selected 173 | const selToggleComponent = addToggleComponent('Replace only in selection', 'If enabled, replaces only occurances in the currently selected text', noSelection); 174 | 175 | // Create Buttons 176 | const buttonContainerEl = document.createElement(divClass); 177 | buttonContainerEl.addClass(rowClass); 178 | 179 | const submitButtonTarget = document.createElement(divClass); 180 | submitButtonTarget.addClass('button-wrapper'); 181 | submitButtonTarget.addClass(rowClass); 182 | 183 | const cancelButtonTarget = document.createElement(divClass); 184 | cancelButtonTarget.addClass('button-wrapper'); 185 | cancelButtonTarget.addClass(rowClass); 186 | 187 | const submitButtonComponent = new ButtonComponent(submitButtonTarget); 188 | const cancelButtonComponent = new ButtonComponent(cancelButtonTarget); 189 | 190 | cancelButtonComponent.setButtonText('Cancel'); 191 | cancelButtonComponent.onClick(() => { 192 | logger('Action cancelled.', 8); 193 | this.close(); 194 | }); 195 | 196 | submitButtonComponent.setButtonText('Replace All'); 197 | submitButtonComponent.setCta(); 198 | submitButtonComponent.onClick(() => { 199 | let resultString = 'No match'; 200 | let scope = ''; 201 | const searchString = findInputComponent.getValue(); 202 | let replaceString = replaceWithInputComponent.getValue(); 203 | const selectedText = editor.getSelection(); 204 | 205 | if (searchString === '') { 206 | new Notice('Nothing to search for!'); 207 | return; 208 | } 209 | 210 | // Replace line breaks in find-field if option is enabled 211 | if (this.settings.processLineBreak) { 212 | logger('Replacing linebreaks in replace-field', 9); 213 | logger(' old: ' + replaceString, 9); 214 | replaceString = replaceString.replace(/\\n/gm, '\n'); 215 | logger(' new: ' + replaceString, 9); 216 | } 217 | 218 | // Replace line breaks in find-field if option is enabled 219 | if (this.settings.processTab) { 220 | logger('Replacing tabs in replace-field', 9); 221 | logger(' old: ' + replaceString, 9); 222 | replaceString = replaceString.replace(/\\t/gm, '\t'); 223 | logger(' new: ' + replaceString, 9); 224 | } 225 | 226 | // Check if regular expressions should be used 227 | if(regToggleComponent.getValue()) { 228 | logger('USING regex with flags: ' + regexFlags, 8); 229 | 230 | const searchRegex = new RegExp(searchString, regexFlags); 231 | if(!selToggleComponent.getValue()) { 232 | logger(' SCOPE: Full document', 9); 233 | const documentText = editor.getValue(); 234 | const rresult = documentText.match(searchRegex); 235 | if (rresult) { 236 | editor.setValue(documentText.replace(searchRegex, replaceString)); 237 | resultString = `Made ${rresult.length} replacement(s) in document`; 238 | } 239 | } 240 | else { 241 | logger(' SCOPE: Selection', 9); 242 | const rresult = selectedText.match(searchRegex); 243 | if (rresult) { 244 | editor.replaceSelection(selectedText.replace(searchRegex, replaceString)); 245 | resultString = `Made ${rresult.length} replacement(s) in selection`; 246 | } 247 | } 248 | } 249 | else { 250 | logger('NOT using regex', 8); 251 | let nrOfHits = 0; 252 | if(!selToggleComponent.getValue()) { 253 | logger(' SCOPE: Full document', 9); 254 | scope = 'selection' 255 | const documentText = editor.getValue(); 256 | const documentSplit = documentText.split(searchString); 257 | nrOfHits = documentSplit.length - 1; 258 | editor.setValue(documentSplit.join(replaceString)); 259 | } 260 | else { 261 | logger(' SCOPE: Selection', 9); 262 | scope = 'document'; 263 | const selectedSplit = selectedText.split(searchString); 264 | nrOfHits = selectedSplit.length - 1; 265 | editor.replaceSelection(selectedSplit.join(replaceString)); 266 | } 267 | resultString = `Made ${nrOfHits} replacement(s) in ${scope}`; 268 | } 269 | 270 | // Saving settings (find/replace text and toggle switch states) 271 | this.settings.findText = searchString; 272 | this.settings.replaceText = replaceString; 273 | this.settings.useRegEx = regToggleComponent.getValue(); 274 | this.settings.selOnly = selToggleComponent.getValue(); 275 | this.plugin.saveData(this.settings); 276 | 277 | this.close(); 278 | new Notice(resultString); 279 | }); 280 | 281 | // Apply settings 282 | regToggleComponent.setValue(this.settings.useRegEx); 283 | selToggleComponent.setValue(this.settings.selOnly); 284 | replaceWithInputComponent.setValue(this.settings.replaceText); 285 | 286 | // Check if the prefill find option is enabled and the selection does not contain linebreaks 287 | if (this.settings.prefillFind && editor.getSelection().indexOf('\n') < 0 && !noSelection) { 288 | logger('Found selection without linebreaks and option is enabled -> fill',9); 289 | findInputComponent.setValue(editor.getSelection()); 290 | selToggleComponent.setValue(false); 291 | } 292 | else { 293 | logger('Restore find text', 9); 294 | findInputComponent.setValue(this.settings.findText); 295 | } 296 | 297 | // Add button row to dialog 298 | buttonContainerEl.appendChild(submitButtonTarget); 299 | buttonContainerEl.appendChild(cancelButtonTarget); 300 | contentEl.appendChild(buttonContainerEl); 301 | 302 | // If no text is selected, disable selection-toggle-switch 303 | if (noSelection) selToggleComponent.setValue(false); 304 | } 305 | 306 | onClose() { 307 | const { contentEl } = this; 308 | contentEl.empty(); 309 | } 310 | } 311 | 312 | class RegexFindReplaceSettingTab extends PluginSettingTab { 313 | plugin: RegexFindReplacePlugin; 314 | 315 | constructor(app: App, plugin: RegexFindReplacePlugin) { 316 | super(app, plugin); 317 | this.plugin = plugin; 318 | } 319 | 320 | display(): void { 321 | const {containerEl} = this; 322 | containerEl.empty(); 323 | 324 | containerEl.createEl('h4', {text: 'Regular Expression Settings'}); 325 | 326 | new Setting(containerEl) 327 | .setName('Case Insensitive') 328 | .setDesc('When using regular expressions, apply the \'/i\' modifier for case insensitive search)') 329 | .addToggle(toggle => toggle 330 | .setValue(this.plugin.settings.caseInsensitive) 331 | .onChange(async (value) => { 332 | logger('Settings update: caseInsensitive: ' + value); 333 | this.plugin.settings.caseInsensitive = value; 334 | await this.plugin.saveSettings(); 335 | })); 336 | 337 | containerEl.createEl('h4', {text: 'General Settings'}); 338 | 339 | 340 | new Setting(containerEl) 341 | .setName('Process \\n as line break') 342 | .setDesc('When \'\\n\' is used in the replace field, a \'line break\' will be inserted accordingly') 343 | .addToggle(toggle => toggle 344 | .setValue(this.plugin.settings.processLineBreak) 345 | .onChange(async (value) => { 346 | logger('Settings update: processLineBreak: ' + value); 347 | this.plugin.settings.processLineBreak = value; 348 | await this.plugin.saveSettings(); 349 | })); 350 | 351 | 352 | new Setting(containerEl) 353 | .setName('Prefill Find Field') 354 | .setDesc('Copy the currently selected text (if any) into the \'Find\' text field. This setting is only applied if the selection does not contain linebreaks') 355 | .addToggle(toggle => toggle 356 | .setValue(this.plugin.settings.prefillFind) 357 | .onChange(async (value) => { 358 | logger('Settings update: prefillFind: ' + value); 359 | this.plugin.settings.prefillFind = value; 360 | await this.plugin.saveSettings(); 361 | })); 362 | } 363 | } --------------------------------------------------------------------------------