From 81de945a0d76248f54c8428fdc8870f57aaddae4 Mon Sep 17 00:00:00 2001 From: Shaun Martin Date: Sat, 20 Apr 2024 13:57:59 -0500 Subject: [PATCH] add setting for default save location --- main.ts | 20 +++--- manifest-beta.json | 2 +- manifest.json | 2 +- src/const.ts | 5 +- src/formatters.ts | 17 +++-- src/settings.ts | 18 ++++- src/types.ts | 3 +- src/util.ts | 66 +++++++++++++++---- test-resources/vault/.obsidian/workspace.json | 37 ++++++----- 9 files changed, 116 insertions(+), 54 deletions(-) diff --git a/main.ts b/main.ts index 824f195..86a9c46 100644 --- a/main.ts +++ b/main.ts @@ -6,7 +6,7 @@ import { SlurpNewNoteModal } from './src/modals/new-note'; import { fetchHtml, mergeMetadata, parseMarkdown, parseMetadata, parsePage } from './src/parse'; import { SlurpSettingsTab } from './src/settings'; import type { FormatterArgs, IArticle, IFrontMatterSettings, IFrontMatterTagSettings, ISettings, ISettingsV0, TFrontMatterProps } from './src/types'; -import { createFilePath } from './src/util'; +import { getNewFilePath, removeTrailingSlash } from './src/util'; export default class SlurpPlugin extends Plugin { settings!: ISettings; @@ -37,12 +37,6 @@ export default class SlurpPlugin extends Plugin { onunload() { } - fixTagPrefix(tagPrefix: string) { - return tagPrefix.endsWith('/') - ? tagPrefix.substring(0, tagPrefix.length - 1) - : tagPrefix; - } - migrateSettingsV0toV1(loadedSettings: Object): ISettings { // only v0 lacks the settingsVersion key if (Object.keys(loadedSettings).contains("settingsVersion")) return loadedSettings as ISettings; @@ -52,7 +46,7 @@ export default class SlurpPlugin extends Plugin { const fmTags = { parse: v0.parseTags, - prefix: this.fixTagPrefix(v0.tagPrefix), + prefix: removeTrailingSlash(v0.tagPrefix), case: v0.tagCase } as IFrontMatterTagSettings; @@ -71,6 +65,11 @@ export default class SlurpPlugin extends Plugin { return v1; } + patchInDefaults() { + if (this.settings.defaultPath === undefined) + this.settings.defaultPath = DEFAULT_SETTINGS.defaultPath; + } + migrateObjToMap(obj: Object) { if (!obj.hasOwnProperty('keys')) { if (Object.keys(obj).length === 0) @@ -91,6 +90,7 @@ export default class SlurpPlugin extends Plugin { const preSettings = Object.assign({}, await this.loadData()); // this.logger.debug("pre-migration settings", preSettings); this.settings = this.migrateSettings(preSettings); + this.patchInDefaults(); this.logger = new Logger(this); this.logger.debug("post-migration settings", this.settings); @@ -101,7 +101,7 @@ export default class SlurpPlugin extends Plugin { } async saveSettings() { - this.settings.fm.tags.prefix = this.fixTagPrefix(this.settings.fm.tags.prefix); + this.settings.fm.tags.prefix = removeTrailingSlash(this.settings.fm.tags.prefix); this.settings.fm.properties = createFrontMatterPropSettings(this.fmProps); this.logger.debug("saving settings", this.settings); await this.saveData(this.settings); @@ -143,7 +143,7 @@ export default class SlurpPlugin extends Plugin { const content = `---\n${frontMatter}\n---\n\n${article.content}`; this.logger.debug("writing file..."); - const filePath = await createFilePath(this.app.vault, article.title); + const filePath = await getNewFilePath(this.app.vault, article.title, this.settings.defaultPath); const newFile = await this.app.vault.create(filePath, content); this.app.workspace.getActiveViewOfType(MarkdownView)?.leaf.openFile(newFile); } diff --git a/manifest-beta.json b/manifest-beta.json index 64b714a..c5b3457 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -1,7 +1,7 @@ { "id": "slurp", "name": "Slurp", - "version": "0.1.6b0", + "version": "0.1.6", "minAppVersion": "0.15.0", "description": "Slurps webpages and saves them as clean, uncluttered Markdown.", "author": "inhumantsar", diff --git a/manifest.json b/manifest.json index 982a4c2..66eedaf 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "slurp", "name": "Slurp", - "version": "0.1.5", + "version": "0.1.6", "minAppVersion": "0.15.0", "description": "Slurps webpages and saves them as clean, uncluttered Markdown.", "author": "inhumantsar", diff --git a/src/const.ts b/src/const.ts index 2c86958..01222d7 100644 --- a/src/const.ts +++ b/src/const.ts @@ -73,6 +73,7 @@ const FRONT_MATTER_ITEM_DEFAULT_SETTINGS = createFrontMatterPropSettings(createF export const DEFAULT_SETTINGS: ISettings = { settingsVersion: 1, + defaultPath: "Slurped Pages", fm: { includeEmpty: false, tags: { @@ -83,6 +84,4 @@ export const DEFAULT_SETTINGS: ISettings = { properties: FRONT_MATTER_ITEM_DEFAULT_SETTINGS }, logs: { logPath: "_slurplogs", debug: false } -} - -export const DEFAULT_PATH: string = "Slurped Pages"; \ No newline at end of file +} \ No newline at end of file diff --git a/src/formatters.ts b/src/formatters.ts index 9b12570..d208cd7 100644 --- a/src/formatters.ts +++ b/src/formatters.ts @@ -40,15 +40,22 @@ export const formatDate = (t = "YYYY-MM-DDTHH:mm", v = new Date()) => { return isNaN(+result) ? result : +result; } -export const formatString = (tmpl: string, val: any) => formatStrings(tmpl, [{ s: val }])[0]; +export const formatString = (tmpl: string, val: any) => + val + ? formatStrings(tmpl, [{ s: val }])[0] + : ""; export const formatStrings = (tmpl: string, val: Iterable): Array => { const result = new Array(); for (let i of val) { - const s = tmpl.replace(/\{(\w+)\}/g, (match, name) => { - return i.hasOwnProperty(name) ? i[name] : match; - }); - result.push(s); + result.push( + tmpl.replace(/\{(\w+)\}/g, (match, name) => + !i.hasOwnProperty(name) + ? match + : i[name] !== undefined + ? "" + : i[name] + )); } return result; } diff --git a/src/settings.ts b/src/settings.ts index 14063a5..d070855 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -4,6 +4,7 @@ import FrontMatterSettings from "./components/NotePropSettings.svelte"; import { FrontMatterProp } from "./frontmatter"; import { Logger } from "./logger"; import { StringCaseOptions, type StringCase } from "./string-case"; +import { DEFAULT_SETTINGS } from "./const"; export class SlurpSettingsTab extends PluginSettingTab { plugin: SlurpPlugin; @@ -16,11 +17,24 @@ export class SlurpSettingsTab extends PluginSettingTab { } display(): void { - const { containerEl } = this; containerEl.empty(); + new Setting(containerEl).setName('General').setHeading(); + this.app.workspace + new Setting(containerEl) + .setName('Default save location') + .setDesc("What directory should Slurp save pages to? Leave blank to save to the vault's main directory.") + .addText((text) => text + .setValue(this.plugin.settings.defaultPath) + .setPlaceholder(DEFAULT_SETTINGS.defaultPath) + .onChange(async (val) => { + this.plugin.settings.defaultPath = val; + await this.plugin.saveSettings(); + }) + ); + new Setting(containerEl).setName('Properties').setHeading(); new Setting(containerEl) @@ -38,7 +52,7 @@ export class SlurpSettingsTab extends PluginSettingTab { this.logger.debug("onValidate called", props); // update existing const modKeys = props.map((prop) => { - //Object.keys(prop).forEach((key) => console.log(`new: ${prop[key]}, curr: ${this.plugin.slurpProps[prop.id][key]}`)); + //Object.keys(prop).forEach((key) => console.log(`new: ${ prop[key]}, curr: ${ this.plugin.slurpProps[prop.id][key] }`)); this.plugin.fmProps.set(prop.id, prop) return prop.id; }); diff --git a/src/types.ts b/src/types.ts index 27eb690..4d79227 100644 --- a/src/types.ts +++ b/src/types.ts @@ -93,7 +93,8 @@ export interface IFrontMatterValidationErrors { export interface ISettings { - settingsVersion: number, + settingsVersion: number + defaultPath: string fm: IFrontMatterSettings logs: ILogSettings } diff --git a/src/util.ts b/src/util.ts index 0df68d0..b13c9ec 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,7 +1,7 @@ import { Vault, normalizePath } from "obsidian"; -import { DEFAULT_PATH } from "src/const"; import type { FrontMatterProp } from "src/frontmatter"; import type { StringCase } from "src/string-case"; +import { logger } from "./logger"; export const isEmpty = (val: any): boolean => { return val == null @@ -9,20 +9,60 @@ export const isEmpty = (val: any): boolean => { || (typeof val[Symbol.iterator] === 'function' && val.length === 0) } -export const createFilePath = async (vault: Vault, title: string, path: string = DEFAULT_PATH): Promise => { - // increment suffix on duplicated file names... to a point. - const fpLoop = (p: string, fn: string, retries: number): string => { - if (retries == 100) throw "Cowardly refusing to increment past 100."; - const suffix = retries > 0 ? `-${retries}.md` : '.md'; - const fp = normalizePath(`${p}/${fn}${suffix}`); - return vault.getFileByPath(fp) ? fpLoop(p, fn, retries + 1) : fp - } +export const removeTrailingSlash = (str: string) => + str.endsWith('/') + ? str.substring(0, str.length - 1) + : str; + +export const cleanTitle = (title: string) => { + // disallowed characters: * " \ / < > : | ? + return title + // assume that a colons and pipes are most likely a useful separator, eg: + // OpenNeRF: Open Set 3D Neural Scene Segmentation... + // Local News | Botched home sale costs man his real estate license + // Blog|Some Title + .replace(/\s?[\|:]\s?/g, ' - ') + // assume that quotes are used to enclose words/phrases + // eg: Bitcoin prices edges lower after "Halving" concludes + .replace('"', "'") + // assume that others can simply be nuked + .replace(/[\*"\\/<>:\?]/g, ''); + +} + +export const ensureFolderExists = async (vault: Vault, path: string) => { + const existingFolder = vault.getFolderByPath(path); + logger().debug(`getFolderByPath("${path}")`, existingFolder); + return existingFolder !== null + ? existingFolder.path + : path === "" + ? "" + : await (await vault.createFolder(path)).path; + +} + +const handleDuplicates = (vault: Vault, filename: string, retries: number, path: string): string => { + if (retries == 100) throw "Cowardly refusing to increment past 100."; + + const suffix = retries > 0 ? ` (${retries}).md` : '.md'; + const fullPath = path !== "" + ? `${path}/${filename}${suffix}` + : `${filename}${suffix}`; + const normPath = normalizePath(fullPath); + + logger().debug(`checking if path is available: ${normPath}`); + return vault.getFileByPath(normPath) ? handleDuplicates(vault, filename, retries + 1, path) : normPath +} + +export const getNewFilePath = async (vault: Vault, title: string, pathSetting: string): Promise => { + + const titleClean = cleanTitle(title); + logger().debug(`finalised title: ${title}`); - // TODO: add setting for slurped pages folder - const folder = vault.getFolderByPath(path) || await vault.createFolder(path); - const fileName = title.replace(/[\\\/:]/g, '-'); + const path = await ensureFolderExists(vault, pathSetting); + logger().debug(`finalised folder: ${path}`); - return fpLoop(folder.path, fileName, 0); + return handleDuplicates(vault, titleClean, 0, path); }; export const sortFrontMatterItems = (items: FrontMatterProp[]) => items.sort((a, b) => a.idx - b.idx); diff --git a/test-resources/vault/.obsidian/workspace.json b/test-resources/vault/.obsidian/workspace.json index 2a942c5..23373bf 100644 --- a/test-resources/vault/.obsidian/workspace.json +++ b/test-resources/vault/.obsidian/workspace.json @@ -13,7 +13,7 @@ "state": { "type": "markdown", "state": { - "file": "Slurped Pages/The first Apple-approved emulator for the iPhone has arrived... and been pulled-1.md", + "file": "Are SAP users embracing AI (1).md", "mode": "source", "backlinks": false, "source": false @@ -70,7 +70,7 @@ } ], "direction": "horizontal", - "width": 300 + "width": 294.5 }, "right": { "id": "903645595e28f33d", @@ -86,7 +86,7 @@ "state": { "type": "backlink", "state": { - "file": "Slurped Pages/The first Apple-approved emulator for the iPhone has arrived... and been pulled-1.md", + "file": "Are SAP users embracing AI (1).md", "collapseAll": false, "extraContext": false, "sortOrder": "alphabetical", @@ -103,7 +103,7 @@ "state": { "type": "outgoing-link", "state": { - "file": "Slurped Pages/The first Apple-approved emulator for the iPhone has arrived... and been pulled-1.md", + "file": "Are SAP users embracing AI (1).md", "linksCollapsed": false, "unlinkedCollapsed": true } @@ -126,7 +126,7 @@ "state": { "type": "outline", "state": { - "file": "Slurped Pages/The first Apple-approved emulator for the iPhone has arrived... and been pulled-1.md" + "file": "Are SAP users embracing AI (1).md" } } } @@ -149,34 +149,35 @@ }, "active": "3780d90630b6e19d", "lastOpenFiles": [ - "Slurped Pages/The first Apple-approved emulator for the iPhone has arrived... and been pulled.md", - "Slurped Pages/The first Apple-approved emulator for the iPhone has arrived... and been pulled-1.md", + "Are SAP users embracing AI.md", + "Are SAP users embracing AI (1).md", + "OpenNeRF - Open Set 3D Neural Scene Segmentation with Pixel-Wise Features and Rendered Novel Views (1).md", + "Slurped Pages/InvertOrNot - Smart Dark-Mode Image Inversion.md", + "OpenNeRF - Open Set 3D Neural Scene Segmentation with Pixel-Wise Features and Rendered Novel Views.md", + "Slurped Pages/OpenNeRF - Open Set 3D Neural Scene Segmentation with Pixel-Wise Features and Rendered Novel Views (1).md", + "Slurped Pages/OpenNeRF - Open Set 3D Neural Scene Segmentation with Pixel-Wise Features and Rendered Novel Views.md", + "Slurped Pages/OpenNeRF- Open Set 3D Neural Scene Segmentation with Pixel-Wise Features and Rendered Novel Views.md", + "_slurplogs/slurp-2024-04-20.md", + "Slurped Pages/The first Apple-approved emulator for the iPhone has arrived... and been pulled-2.md", "_slurplogs/slurp-2024-04-19.md", + "other path/The first Apple-approved emulator for the iPhone has arrived... and been pulled.md", + "Slurped Pages/The first Apple-approved emulator for the iPhone has arrived... and been pulled-1.md", + "other path", + "Slurped Pages/The first Apple-approved emulator for the iPhone has arrived... and been pulled.md", "Slurped Pages/The first Apple-approved emulator for the iPhone has arrived... and been pulled-6.md", "Slurped Pages/The first Apple-approved emulator for the iPhone has arrived... and been pulled-7.md", "Slurped Pages/The first Apple-approved emulator for the iPhone has arrived... and been pulled-5.md", "Slurped Pages/The first Apple-approved emulator for the iPhone has arrived... and been pulled-4.md", "Slurped Pages/The first Apple-approved emulator for the iPhone has arrived... and been pulled-3.md", - "Slurped Pages/The first Apple-approved emulator for the iPhone has arrived... and been pulled-2.md", "Slurped Pages/The first Apple-approved emulator for the iPhone has arrived... and been pulled-10.md", "Slurped Pages/The first Apple-approved emulator for the iPhone has arrived... and been pulled-11.md", "Slurped Pages/The first Apple-approved emulator for the iPhone has arrived... and been pulled-8.md", "Slurped Pages/The first Apple-approved emulator for the iPhone has arrived... and been pulled-9.md", "_slurplogs/slurp-2024-04-18.md", - "Slurped Pages/OpenNeRF- Open Set 3D Neural Scene Segmentation with Pixel-Wise Features and Rendered Novel Views.md", - "Slurped Pages/InvertOrNot - Smart Dark-Mode Image Inversion.md", "_slurplogs/slurp-2024-04-18.log", "_slurplogs", "Slurped Pages/The first Apple-approved emulator for the iPhone has arrived... and been pulled-12.md", "Slurped Pages/The first Apple-approved emulator for the iPhone has arrived... and been pulled (without tags).md", - "Slurped Pages/The first Apple-approved emulator for the iPhone has arrived... and been pulled (with tags).md", - "Slurped Pages/Show HN- Invertornot.com – API to enhance your images in dark-mode.md", - "Slurped Pages/InvertOrNot - Smart Dark-Mode Image Inversion-3.md", - "Slurped Pages/InvertOrNot - Smart Dark-Mode Image Inversion-4.md", - "Slurped Pages/InvertOrNot - Smart Dark-Mode Image Inversion-2.md", - "Slurped Pages/InvertOrNot - Smart Dark-Mode Image Inversion-1.md", - "Slurped Pages/InvertOrNot - Smart Dark-Mode Image Inversion-5.md", - "Slurped Pages/InvertOrNot - Smart Dark-Mode Image Inversion-6.md", "Slurped Pages" ] } \ No newline at end of file