From 097df387a482128058b868f3aef3aab9a66789e7 Mon Sep 17 00:00:00 2001 From: Daniel Barenholz Date: Sun, 26 Dec 2021 17:06:25 +0100 Subject: [PATCH] Rewrite plugin and properly deregister views. --- README.md | 47 +++++++++++++++----- manifest.json | 6 +-- package.json | 24 +++++----- src/helper.ts | 74 +++++++++++++++++++++---------- src/main.ts | 99 +++++++++++++++++++++++++++++------------- src/settings.ts | 113 ++++++++++++++++++++---------------------------- src/view.ts | 75 ++++++++++++++++++-------------- versions.json | 1 + 8 files changed, 260 insertions(+), 179 deletions(-) diff --git a/README.md b/README.md index 9897c4e..7b8640a 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,44 @@ # Plaintext for Obsidian -Obisidan (https://obsidian.md) plugin that allows it to open files as plaintext. -Developed for Obsidian version 0.12.12. It might work with older versions, but it is not tested. +This is an [Obisidan](https://obsidian.md) plugin that allows you to open _any_ file as plaintext. It has been developed for Obsidian **v0.13.14**, and tested on **Windows**. -Code is functional! You can actually edit plaintext files now. There's no fancy syntax highlighting, since the file -is interpreted as plaintext, as opposed to some other type of text. +Honestly, as long as you can run any Obsidian version you can _probably_ run this plugin as well. The only requirements are that we can register extensions (this existed in v0.12.12 for instance), and that the `viewRegistry` exists, which I'm assuming has been there since the beginning of Obsidian. But, this is all speculation! + +**NOTE: There are other plugins that allow you to edit specific files. MAKE SURE TO NOT TYPE THEIR EXTENSIONS INTO THE SETTINGS FIELD FOR THIS PLUGIN. I cannot (yet) check for specific plugins that have their own view for a particular extension, and as such this plugin WILL overwrite the view, and break the other extension. If you do this by accident, open the plugin folder (`.obsidian/plugins/obsidian-plaintext/`), and remove from the `data.json` file the extensions that you typed by mistake.** + +## Installing + +Interested in editing files in Obsidian? Great. Grab the latest release from the [releases](#) page, and copy `main.js` and `manifest.json` to `.obsidian/plugins/obsidian-plaintext/`. That's it! + +When approved, you can also install this through Obsidian by searching for **plaintext**. + +## Roadmap + +For now, nothing is planned. If you're interested in features, please make an issue on Github! + +## Contributing + +Also excited about making Obsidian a full-fledged IDE? Cool, me too! Contact me and let's talk! Pull requests (especially one that updates the code to use `CodeMirror 6`) are very welcome. + +## Pricing + +This is free. Keep your money, I don't want it. ## Changelog -**Version 0.0.2 (current)**: -* First actual release. -* Code is functional! You can open and edit files as plaintext. +**Version 0.1.0 (current)**: + +- Complete rewrite of registering and deregistering. +- Now _actually_ removes views when deregistering a particular extension. +- Correctly filters out default obsidian extensions: No more accidentally overwriting the default markdown editor. + +**Version 0.0.2**: + +- First actual release. +- Code is functional! You can open and edit files as plaintext. +**Version 0.0.1**: -**Version 0.0.1**: -* Not a release. -* Initial testing code. -* This included the functionality for parsing user-inputted extensions. \ No newline at end of file +- Not a release. +- Initial testing code. +- This included the functionality for parsing user-inputted extensions. diff --git a/manifest.json b/manifest.json index 90de7b8..8e23dec 100644 --- a/manifest.json +++ b/manifest.json @@ -1,9 +1,9 @@ { "id": "obsidian-plaintext", "name": "Plaintext", - "version": "0.0.2", - "minAppVersion": "0.12.12", - "description": "Allow opening specified files as plaintext.", + "version": "0.1.0", + "minAppVersion": "0.13.14", + "description": "Allow opening specified files as plaintext (RAW mode).", "author": "dbarenholz", "authorUrl": "https://github.com/dbarenholz/dbarenholz", "isDesktopOnly": true diff --git a/package.json b/package.json index 9dd0e8f..1e7a491 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-plaintext", - "version": "0.0.2", + "version": "0.1.0", "description": "Allow opening specified files as plaintext.", "main": "main.js", "scripts": { @@ -10,17 +10,15 @@ "keywords": [], "author": "dbarenholz", "license": "MIT", - "devDependencies": { - "@rollup/plugin-commonjs": "^18.0.0", - "@rollup/plugin-node-resolve": "^11.2.1", - "@rollup/plugin-typescript": "^8.2.1", - "@types/node": "^14.14.37", - "obsidian": "^0.12.0", - "rollup": "^2.32.1", - "tslib": "^2.2.0", - "typescript": "^4.2.4" - }, "dependencies": { - "codemirror": "^5.62.3" + "@rollup/plugin-commonjs": "^21.0.1", + "@rollup/plugin-node-resolve": "^13.1.1", + "@rollup/plugin-typescript": "^8.3.0", + "@types/node": "^17.0.4", + "codemirror": "^5.65.0", + "obsidian": "^0.13.11", + "rollup": "^2.62.0", + "tslib": "^2.3.1", + "typescript": "^4.5.4" } -} \ No newline at end of file +} diff --git a/src/helper.ts b/src/helper.ts index 6f402d4..0cfb35f 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -1,24 +1,52 @@ -// Taken directly from here -// https://help.obsidian.md/Advanced+topics/Accepted+file+formats -// On sept 2, 2021. May need updates later. - -export const obsidianExts: string[] = [ +/** + * Extensions obsidian supports natively. + * Taken from the help page: https://help.obsidian.md/Advanced+topics/Accepted+file+formats + * + * @version 0.1.0 + * @author dbarenholz + * @since 2021/12/26 + */ +export const obsidianExts: Set = new Set([ "md", - " png", - " jpg", - " jpeg", - " gif", - " bmp", - " svg", - " mp3", - " webm", - " wav", - " m4a", - " ogg", - " 3gp", - " flac", - " mp4", - " webm", - " ogv", - " pdf", -]; + "png", + "jpg", + "jpeg", + "gif", + "bmp", + "svg", + "mp3", + "webm", + "wav", + "m4a", + "ogg", + "3gp", + "flac", + "mp4", + "webm", + "ogv", + "pdf", +]); + +/** + * Takes in a list of extensions, and removes extensions if they are present in the obsidianExts set. + * + * @param exts extensions to process + * @returns All extensions in exts, except if they're present in obsidianExts. + */ +export const removeObsidianExtensions = (exts: string[]): string[] => { + return exts.filter(ext => !obsidianExts.has(ext)); +} + +declare module 'obsidian' { + interface App { + viewRegistry: { + unregisterView: CallableFunction // ƒ (e){delete this.viewByType[e]} + unregisterExtensions: CallableFunction // ƒ (e){for(var t=0,n=e;t { - console.log("Obsidian Plaintext: loaded plugin."); + console.log("[Plaintext]: loaded plugin."); // Load the settings await this.loadSettings(); @@ -27,15 +30,17 @@ export default class PlaintextPlugin extends Plugin { // Add settings tab this.addSettingTab(new PlaintextSettingTab(this.app, this)); - // Do the work - this.processExts(this.settings.extensions); + // Add extensions that we need to add. + this.addExtensions(this.settings.extensions); } /** * Code that runs (once) when the plugin is unloaded. */ onunload(): void { - console.log("Obsidian Plaintext: unloaded plugin."); + // cleanup + this.removeExtensions(this.settings.extensions); + console.log("[Plaintext]: unloaded plugin."); } /** @@ -54,52 +59,88 @@ export default class PlaintextPlugin extends Plugin { /** * Creates a view for a plaintext file. - * Plaintext views have NO syntax highlighting or other fancy features! * * @param leaf The leaf to create the view at - * @param ext Plaintext extension * @returns Plaintext view */ - viewCreator: ViewCreator = (leaf: WorkspaceLeaf, ext?: string): PlaintextView => { - return new PlaintextView(leaf, ext); + viewCreator: ViewCreator = (leaf: WorkspaceLeaf): PlaintextView => { + return new PlaintextView(leaf); }; /** - * Processes the extensions. - * - * @param exts extensions + * Registers extensions, and makes views for them. + * + * @param exts The extensions to register and add views for. */ - processExts = (exts: string[]): void => { - if (exts.length == 0) { - console.log("Plaintext: No extensions to process."); - return; - } - - for (const ext of exts) { - // Disallow using obsidian defaults - if (ext in obsidianExts) { - if (this.settings.debug) { - console.log(`Plaintext: Extension '${ext}' is used by Obsidian already! Don't override Obsidian.`); - } - } + addExtensions = (exts: string[]): void => { + // Remove obsidian exts just in case + exts = removeObsidianExtensions(exts) + // Loop through extensions + exts.forEach((ext) => { // Try to register view try { this.registerView(ext, this.viewCreator); } catch { if (this.settings.debug) { - console.log(`Plaintext: Extension '${ext}' already has a view registered, ignoring...`); + console.log(`[Plaintext]: Extension '${ext}' already has a view registered, ignoring...`); } } // Try to register extension try { + // Note: viewtype is set to 'ext' here for possible future expansion to include syntax highlighting based on extension type. this.registerExtensions([ext], ext); } catch { if (this.settings.debug) { - console.log(`Plaintext: Extension '${ext}' is already registered, ignoring...`); + console.log(`[Plaintext]: Extension '${ext}' is already registered, ignoring...`); + } + } + + // Logging + if (this.settings.debug) { + console.log(`[Plaintext]: added=${ext}`); + } + }) + }; + + /** + * Deregisters extensions, and removes views made for them. + * + * @param exts The extensions to deregister and remove views for. + */ + removeExtensions = (exts: string[]): void => { + // Remove obsidian exts just in case + exts = removeObsidianExtensions(exts) + + // Try to deregister the views + exts.forEach((ext) => { + // Before unregistering the view: close active leaf if of type ext + if (ext == this.app.workspace.activeLeaf.view.getViewType()) { + this.app.workspace.activeLeaf.detach(); + } + + try { + this.app.viewRegistry.unregisterView(ext) + } catch { + if (this.settings.debug) { + console.log(`[Plaintext]: View for extension '${ext}' cannot be deregistered...`); } } + }); + + // Try to deregister the extensions + try { + this.app.viewRegistry.unregisterExtensions(exts) + } catch { + if (this.settings.debug) { + console.log(`[Plaintext]: Cannot deregister extensions...`); + } + } + + // Logging + if (this.settings.debug) { + console.log(`[Plaintext]: removed=${exts}`); } }; } diff --git a/src/settings.ts b/src/settings.ts index 00f6aa2..ff86b85 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,10 +1,15 @@ import PlaintextPlugin from "./main"; import { App, PluginSettingTab, Setting } from "obsidian"; +import { removeObsidianExtensions } from "./helper"; +import { nextTick } from "process"; /** - * Possible settings for the plaintext plugin. + * Plaintext plugin settings. + * + * Currently, there are only settings on whether to print debug prints to console, + * and a list of extensions that should be considered plaintext. * - * @version 0.0.1 + * @version 0.1.0 * @author dbarenholz */ export interface PlaintextSettings { @@ -16,9 +21,9 @@ export interface PlaintextSettings { } /** - * Default settings. + * The defaults: no debugging, and no extensions to consider for the plaintext plugin. * - * @version 0.0.1 + * @version 0.1.0 * @author dbarenholz */ export const DEFAULT_SETTINGS: PlaintextSettings = { @@ -29,27 +34,34 @@ export const DEFAULT_SETTINGS: PlaintextSettings = { /** * The settings tab itself. * - * @version 0.0.2 + * @version 0.1.0 * @author dbarenholz */ export class PlaintextSettingTab extends PluginSettingTab { - plugin: PlaintextPlugin; - changes: string; + // The plugin itself + private plugin: PlaintextPlugin; + // Changes made to the extension array + private changes: string; + + // Constructor: Creates a settingtab for this plugin. constructor(app: App, plugin: PlaintextPlugin) { super(app, plugin); this.plugin = plugin; this.changes = null; } - + /** + * The method called to display the settingtab. + */ display(): void { + // Retrieve the container element let { containerEl } = this; - containerEl.empty(); + // Write the title of the settings page. containerEl.createEl("h2", { text: "Plaintext" }); - // Debug settings + // Add debug setting new Setting(containerEl) .setName("Debug") .setDesc("Turn on for debug prints in console.") @@ -62,11 +74,13 @@ export class PlaintextSettingTab extends PluginSettingTab { }); }); - // Extension settings + // Add extension setting new Setting(containerEl) .setName("Extensions") .setDesc( - "List of extensions to interpret as plaintext, comma-separated. Will automatically convert to a set when reopening the Obsidian Plaintext settings window. Obsidian default extensions are filtered out!" + "List of extensions to interpret as plaintext, comma-separated." + + " Will automatically convert to a set when reopening the Obsidian Plaintext settings window." + + " Obsidian's default extensions are filtered out!" ) .addText((text) => { text @@ -74,8 +88,10 @@ export class PlaintextSettingTab extends PluginSettingTab { .setValue(Array.from(this.plugin.settings.extensions).toString()) .onChange((value) => (this.changes = value.toLowerCase().trim())); + + // Can't seem to set to a separate function due to incorrect `this` text.inputEl.onblur = async () => { - // Grab set of saved extensions + // Get the currently enabled extensions from the plaintext plugin. let current_exts = Array.from(this.plugin.settings.extensions); current_exts = current_exts == [] || current_exts == null || current_exts == undefined @@ -83,81 +99,44 @@ export class PlaintextSettingTab extends PluginSettingTab { : Array.from(new Set(current_exts)); if (this.plugin.settings.debug) { - console.log(`Current exts: ${Array.from(this.plugin.settings.extensions).toString()}`); + console.log(`[Plaintext]: Current exts=${Array.from(this.plugin.settings.extensions).toString()}`); } - // Grab set of new extensions - let new_exts = this.changes + // Grab the set of new extensions + let new_exts = this.changes == null || this.changes == undefined ? [] : removeObsidianExtensions(this.changes .split(",") // split on comma .map((s) => s.toLowerCase().trim()) // convert to lowercase and remove spaces - .filter((s) => s != ""); // remove empty elements - - // Set-ify - new_exts = Array.from(new Set(new_exts)); + .filter((s) => s != "")); // remove empty elements if (this.plugin.settings.debug) { - console.log(`New exts: ${new_exts}`); + console.log(`[Plaintext]: New exts=${new_exts}`); } - // Extensions that should be added - let to_add: string[] = []; - - new_exts.forEach((nExt) => { - // New is also present in current -- no change - if (current_exts.includes(nExt)) { - // do nothing - } else { - // New is NOT present in current -- ADD! - to_add.push(nExt); - } - }); + // Find which extensions to add. + let to_add = new_exts.filter(nExt => !current_exts.includes(nExt)) if (this.plugin.settings.debug) { - console.log(`To add: ${to_add}`); + console.log(`[Plaintext]: add=${to_add}`); } - // Extensions that should be removed - let to_remove: string[] = []; + // Actually add the extensions + this.plugin.addExtensions(to_add) - current_exts.forEach((cExt) => { - // Current is also present in new -- no change - if (new_exts.includes(cExt)) { - // do nothing - } else { - // Current is NOT present in new -- REMOVE! - to_remove.push(cExt); - } - }); + // Find which extensions to remove. + let to_remove = current_exts.filter(cExt => !new_exts.includes(cExt)) if (this.plugin.settings.debug) { - console.log(`To remove: ${to_remove}`); + console.log(`[Plaintext]: remove=${to_remove}`); } - // Actually add the extensions - to_add.forEach((nExt) => { - current_exts.push(nExt); - - if (this.plugin.settings.debug) { - console.log(`Added: ${nExt}`); - } - }); - // Actually remove the extensions - to_remove.forEach((cExt) => { - current_exts.remove(cExt); - - if (this.plugin.settings.debug) { - console.log(`Removed: ${cExt}`); - } - }); + this.plugin.removeExtensions(to_remove) // Save settings - this.plugin.settings.extensions = current_exts; + const updated_exts = current_exts.concat(to_add).filter((ext) => !to_remove.includes(ext)) + this.plugin.settings.extensions = updated_exts; await this.plugin.saveSettings(); - - // Do the work - this.plugin.processExts(this.plugin.settings.extensions); - }; + } }); } } diff --git a/src/view.ts b/src/view.ts index e51cc5b..ba021d3 100644 --- a/src/view.ts +++ b/src/view.ts @@ -2,38 +2,30 @@ import CodeMirror from "codemirror"; import { TextFileView, WorkspaceLeaf } from "obsidian"; /** - * The view used for plaintext files. - * Editing is facilitated with a codemirror instance, with minimal settings. + * The view used for plaintext files. Uses a CodeMirror 5 instance. + * Perhaps this can be updated to CodeMirror 6 in the future. * * @author dbarenholz - * @version 0.0.2 + * @version 0.1.0 */ export default class PlaintextView extends TextFileView { // Internal codemirror instance - codeMirror: CodeMirror.Editor; - - // Current file extension - ext: string; + public cm: CodeMirror.Editor; // Constructor - constructor(leaf: WorkspaceLeaf, ext: string) { + constructor(leaf: WorkspaceLeaf) { // Call super super(leaf); // Create code mirror instance and add listener to it. - // TODO: Check if this theme needs to be added or not - // this.codeMirror = CodeMirror(this.contentEl, { - // theme: "obsidian", - // }); - this.codeMirror = CodeMirror(this.contentEl); - this.codeMirror.on("changes", this.changed); - - // Save extension - this.ext = ext; + this.cm = CodeMirror(this.contentEl); + this.cm.on("changes", this.changed); } /** - * Event handler for CodeMirror editor. Requests a save. + * Event handler for CodeMirror editor. + * Requests a save. + * * @param _ unused * @param __ unused */ @@ -42,10 +34,11 @@ export default class PlaintextView extends TextFileView { }; /** - * Event handler for resizing a view. Refreshes codemirror. + * Event handler for resizing a view. + * Refreshes codemirror instance. */ onResize(): void { - this.codeMirror.refresh(); + this.cm.refresh(); } /** @@ -55,7 +48,7 @@ export default class PlaintextView extends TextFileView { * @returns The file contents as string. */ getViewData = (): string => { - return this.codeMirror.getValue(); + return this.cm.getValue(); }; /** @@ -71,31 +64,47 @@ export default class PlaintextView extends TextFileView { */ setViewData = (data: string, clear?: boolean): void => { if (clear) { - // Hardcoded MIME type. Everything is plain text. - this.codeMirror.swapDoc(CodeMirror.Doc(data, "text/plain")); + this.cm.swapDoc(CodeMirror.Doc(data, "text/plain")); // everything is plaintext } else { - this.codeMirror.setValue(data); + this.cm.setValue(data); } }; - // Clearing this particular item is setting the value to an empty string + /** + * Clears the current codemirror instance. + */ clear = (): void => { - this.codeMirror.setValue(""); - this.codeMirror.clearHistory(); + this.cm.setValue(""); + this.cm.clearHistory(); }; - // This method doesn't really do much for our usecase... + /** + * Provides a boolean to indicate if a particular extension can be opened in this instance. + * + * @param extension the extension to check + * @returns `true` if `extension` is identical to `this.ext`, `false` otherwise. + */ canAcceptExtension(extension: string): boolean { - return extension == this.ext; + return extension == this.file.extension; } - // Returns the extension + /** + * Returns the viewtype of this codemirror instance. + * The viewtype is the extension of the file that is opened. + * + * @returns The viewtype (file extension) of this codemirror instance. + */ getViewType(): string { - return this.ext; + return this.file ? this.file.extension : "text/plain (no file)"; } - // Returns the basename of the open file, or "plaintext" if it doesn't exist + /** + * Returns a string indicating which file is currently open, if any. + * If no file is open, returns that. + * + * @returns A string indicating the opened file, if any. + */ getDisplayText(): string { - return this.file ? this.file.basename : "plaintext (no file)"; + return this.file ? this.file.basename : "text/plain (no file)"; } } diff --git a/versions.json b/versions.json index bb31ebb..82ec482 100644 --- a/versions.json +++ b/versions.json @@ -1,4 +1,5 @@ { + "0.1.0": "0.13.14", "0.0.2": "0.12.12", "0.0.1": "0.12.12" } \ No newline at end of file