diff --git a/README.md b/README.md index 4de57b5..1167401 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ ![Obsidian Link Range Logo](https://user-images.githubusercontent.com/23059902/225677761-c36b01a6-6194-4d83-a130-a1d7561b8359.png) -This plugin brings wiki-link header range queries to Obsidian while retaining native backlink functionality. Supports standard wiki-links, hover preview, and embedded content wiki-links. +This plugin brings wiki-link header range queries to Obsidian while retaining native backlink functionality. Supports standard wiki-links or other user-defined patterns, hover preview, and embedded content wiki-links. -![demo](./docs/demo.gif) +![demo](./docs/demo-2.gif) ## Usage To use, simply use the same wiki-link syntax you're used to in obsidian, but instead of specifying a single header, you can specify a header range separated by a separator string. @@ -13,9 +13,16 @@ For example, if I wanted a link to headers "Ingredients" through "Directions" in `[[Recipe#Ingredients..Directions]]` -## Settings -| Setting | Description | Default Value | -| ------------------------------------------- | ------ | ------ | -| Heading Separator | Defines the separator to be used to define a link heading range. I.e. this is the ".." in example: `[[Note 1#h1..h2]]` | `..` | -| Alt Text Format | The alternate text format that gest shown when in read mode. Use $note for the note name placeholder, and $h1/$h2 for the header placeholders. | `$note:$h1-$h2` | -| End Inclusive | When turned on, the second header and the content following it (through the next header in the note) is shown. | true | +## Basic Settings + +The default settings give you the functionality indicated above. + +![default settings](./docs/default-settings.png) + +## Advanced Settings Choices + +If you prefer a different heading separator than the Wikilink syntax, you may change it. + +Some will want to change the visual style of the heading separators. The + +![advanced settings](./docs/advanced-settings.png) diff --git a/docs/advanced-settings.png b/docs/advanced-settings.png new file mode 100644 index 0000000..4337846 Binary files /dev/null and b/docs/advanced-settings.png differ diff --git a/docs/default-settings.png b/docs/default-settings.png new file mode 100644 index 0000000..46962a6 Binary files /dev/null and b/docs/default-settings.png differ diff --git a/docs/demo-2.gif b/docs/demo-2.gif new file mode 100644 index 0000000..43baae7 Binary files /dev/null and b/docs/demo-2.gif differ diff --git a/main.ts b/main.ts index 6def120..b4f0821 100644 --- a/main.ts +++ b/main.ts @@ -4,7 +4,9 @@ import { ViewPlugin } from "@codemirror/view"; import { DEFAULT_SETTINGS, LinkRangeSettings, LinkRangeSettingTab } from 'src/settings'; import { linkRangePostProcessor } from 'src/markdownPostProcessor'; import { checkLink } from 'src/utils'; -import { LinkRangeView } from 'src/linkRangeView'; +import { LifePreviewEmbedReplacer } from 'src/livePreviewEmbedReplacer'; +import { buildCMViewPlugin } from 'src/livePreviewDisplayView'; +import { Prec } from "@codemirror/state"; export default class LinkRange extends Plugin { settings: LinkRangeSettings; @@ -25,9 +27,12 @@ export default class LinkRange extends Plugin { // wait for layout to be ready this.app.workspace.onLayoutReady(() => { this.registerEditorExtension(ViewPlugin.define((v) => { - return new LinkRangeView(this.settings, this.app) + return new LifePreviewEmbedReplacer(this.settings, this.app) })); + const ext = Prec.lowest(buildCMViewPlugin(this.app, this.settings)); + this.registerEditorExtension(ext); + const pagePreviewPlugin = this.app.internalPlugins.plugins["page-preview"]; console.log("LinkRange: Hooking into page-preview onHover calls") diff --git a/manifest.json b/manifest.json index 2af52c7..c41e587 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,10 @@ { "id": "link-range", "name": "Link Range", - "version": "1.0.1", + "version": "2.0.0", "minAppVersion": "0.15.0", - "description": "This Obsidian plugin brings ranged wiki-link support to Obsidian.", - "author": "Ryan Mellmer", - "authorUrl": "https://ryanmellmer.com", + "description": "This Obsidian plugin brings ranged link support to Obsidian.", + "author": "Ryan Mellmer and Eric Sowell", + "authorUrl": "https://ryanmellmer.com, https://ericsowell.com", "isDesktopOnly": false } diff --git a/package-lock.json b/package-lock.json index 0d59931..aa9f11b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "obsidian-sample-plugin", "version": "1.0.0", + "hasInstallScript": true, "license": "MIT", "dependencies": { "@types/codemirror": "^5.60.7", @@ -14,6 +15,7 @@ "monkey-around": "^2.3.0" }, "devDependencies": { + "@codemirror/language": "6.10.1", "@types/node": "^16.11.6", "@typescript-eslint/eslint-plugin": "5.29.0", "@typescript-eslint/parser": "5.29.0", @@ -54,13 +56,13 @@ } }, "node_modules/@codemirror/language": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.6.0.tgz", - "integrity": "sha512-cwUd6lzt3MfNYOobdjf14ZkLbJcnv4WtndYaoBkbor/vF+rCNguMPK0IRtvZJG4dsWiaWPcK8x1VijhvSxnstg==", + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.1.tgz", + "integrity": "sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==", "dependencies": { "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/common": "^1.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" @@ -87,17 +89,17 @@ } }, "node_modules/@codemirror/state": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.2.0.tgz", - "integrity": "sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA==" + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.0.tgz", + "integrity": "sha512-hm8XshYj5Fo30Bb922QX9hXB/bxOAVH+qaqHBzw5TKa72vOeslyGwd4X8M0c1dJ9JqxlaMceOQ8RsL9tC7gU0A==" }, "node_modules/@codemirror/view": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.9.1.tgz", - "integrity": "sha512-bzfSjJn9dAADVpabLKWKNmMG4ibyTV2e3eOGowjElNPTdTkSbi6ixPYHm2u0ADcETfKsi2/R84Rkmi91dH9yEg==", + "version": "6.24.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.24.0.tgz", + "integrity": "sha512-zK6m5pNkdhdJl8idPP1gA4N8JKTiSsOz8U/Iw+C1ChMwyLG7+MLiNXnH/wFuAk6KeGEe33/adOiAh5jMqee03w==", "dependencies": { - "@codemirror/state": "^6.1.4", - "style-mod": "^4.0.0", + "@codemirror/state": "^6.4.0", + "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, @@ -524,9 +526,9 @@ "peer": true }, "node_modules/@lezer/common": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.2.tgz", - "integrity": "sha512-SVgiGtMnMnW3ActR8SXgsDhw7a0w0ChHSYAyAUxxrOiJ1OqYWEKk/xJd84tTSPo1mo6DXLObAJALNnd0Hrv7Ng==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", + "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==" }, "node_modules/@lezer/highlight": { "version": "1.1.3", @@ -2374,9 +2376,9 @@ } }, "node_modules/style-mod": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz", - "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.0.tgz", + "integrity": "sha512-Ca5ib8HrFn+f+0n4N4ScTIA9iTOQ7MaGS1ylHcoVqW9J7w2w8PzN6g9gKmTYgGEBH8e120+RCmhpje6jC5uGWA==" }, "node_modules/supports-color": { "version": "7.2.0", @@ -2596,13 +2598,13 @@ } }, "@codemirror/language": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.6.0.tgz", - "integrity": "sha512-cwUd6lzt3MfNYOobdjf14ZkLbJcnv4WtndYaoBkbor/vF+rCNguMPK0IRtvZJG4dsWiaWPcK8x1VijhvSxnstg==", + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.1.tgz", + "integrity": "sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==", "requires": { "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/common": "^1.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" @@ -2629,17 +2631,17 @@ } }, "@codemirror/state": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.2.0.tgz", - "integrity": "sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA==" + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.0.tgz", + "integrity": "sha512-hm8XshYj5Fo30Bb922QX9hXB/bxOAVH+qaqHBzw5TKa72vOeslyGwd4X8M0c1dJ9JqxlaMceOQ8RsL9tC7gU0A==" }, "@codemirror/view": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.9.1.tgz", - "integrity": "sha512-bzfSjJn9dAADVpabLKWKNmMG4ibyTV2e3eOGowjElNPTdTkSbi6ixPYHm2u0ADcETfKsi2/R84Rkmi91dH9yEg==", + "version": "6.24.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.24.0.tgz", + "integrity": "sha512-zK6m5pNkdhdJl8idPP1gA4N8JKTiSsOz8U/Iw+C1ChMwyLG7+MLiNXnH/wFuAk6KeGEe33/adOiAh5jMqee03w==", "requires": { - "@codemirror/state": "^6.1.4", - "style-mod": "^4.0.0", + "@codemirror/state": "^6.4.0", + "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, @@ -2849,9 +2851,9 @@ "peer": true }, "@lezer/common": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.2.tgz", - "integrity": "sha512-SVgiGtMnMnW3ActR8SXgsDhw7a0w0ChHSYAyAUxxrOiJ1OqYWEKk/xJd84tTSPo1mo6DXLObAJALNnd0Hrv7Ng==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", + "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==" }, "@lezer/highlight": { "version": "1.1.3", @@ -4200,9 +4202,9 @@ "peer": true }, "style-mod": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz", - "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.0.tgz", + "integrity": "sha512-Ca5ib8HrFn+f+0n4N4ScTIA9iTOQ7MaGS1ylHcoVqW9J7w2w8PzN6g9gKmTYgGEBH8e120+RCmhpje6jC5uGWA==" }, "supports-color": { "version": "7.2.0", diff --git a/package.json b/package.json index b43b578..2fd396d 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "obsidian": "latest", "patch-package": "^6.5.1", "tslib": "2.4.0", - "typescript": "4.7.4" + "typescript": "4.7.4", + "@codemirror/language": "6.10.1" }, "dependencies": { "@types/codemirror": "^5.60.7", diff --git a/src/embeds.ts b/src/embeds.ts index fe43374..2fcb287 100644 --- a/src/embeds.ts +++ b/src/embeds.ts @@ -7,7 +7,8 @@ export async function replaceEmbed(app: App, embed: Node, settings: LinkRangeSet const res = checkLink(app, embedHtml, settings, true, "src"); - if (res !== null) { + const isLinkRange = res !== null && res.h2 !== undefined; + if (isLinkRange) { const { vault } = app; const foundNote : TFile | undefined = app.vault.getMarkdownFiles().filter( x => x.basename == res.note diff --git a/src/livePreviewDisplayView.ts b/src/livePreviewDisplayView.ts new file mode 100644 index 0000000..1c2154f --- /dev/null +++ b/src/livePreviewDisplayView.ts @@ -0,0 +1,147 @@ +import { App } from 'obsidian'; +import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate, WidgetType } from "@codemirror/view"; +import { RangeSetBuilder } from "@codemirror/state"; +import { syntaxTree } from "@codemirror/language"; +import { LinkRangeSettings } from "./settings"; +import { findPatternForFile } from './utils'; + +class CharacterOverwriteWidget extends WidgetType { + + private char: string; + constructor(char: string) { + super(); + this.char = char; + } + + toDOM() { + let el = document.createElement("span"); + el.innerText = this.char; + el.style.textDecoration = 'underline'; + return el; + } +} + +export function buildCMViewPlugin(app: App, settings: LinkRangeSettings) { + + const viewPlugin = ViewPlugin.fromClass( + class { + decorations: DecorationSet; + decoratedRanges: Array<{ from: number, to: number }>; + lastLocation: { from: number; to: number; }; + + constructor(view: EditorView) { + this.decorations = this.buildDecorations(view, null); + this.lastLocation = { from: 0, to: 0 };; + } + + update(update: ViewUpdate) { + let currentLocation = { + from: update.state.selection.ranges[0].from, + to: update.state.selection.ranges[0].to + }; + if (update.docChanged || update.viewportChanged) { + this.decorations = this.buildDecorations(update.view, currentLocation); + } + else { + // isRepeatUpdate - avoid repeated updates and renders + let isRepeatUpdate = this.lastLocation.from == currentLocation.from && this.lastLocation.to === currentLocation.to; + if (update.state.selection.ranges.length > 0 && !isRepeatUpdate) { + this.decorations = this.buildDecorations(update.view, currentLocation); + } + } + this.lastLocation = currentLocation; + } + + buildDecorations(view: EditorView, location: { from: number, to: number } | null): DecorationSet { + let builder = new RangeSetBuilder(); + + const lastPassDecoratedRanges: Array<{from: number, to: number}> = this.decoratedRanges; + this.decoratedRanges = []; + + const inLastPass = function(nodeStart: number, index: number | undefined): boolean { + if (index === undefined) { + return false; + } + + for (let i in lastPassDecoratedRanges) { + const rng = lastPassDecoratedRanges[i]; + + if (rng.from == nodeStart && index >= rng.from && index <= rng.to) { + return true; + } + } + return false; + } + + for (let {from, to} of view.visibleRanges) { + + syntaxTree(view.state).iterate({ + from, + to, + enter: (node) => { + // Big thanks to Supercharged Links. Used your code as an example! https://github.com/mdelobelle/obsidian_supercharged_links + + // TODO: there used to be a different way than splitting the name but the api for this has + // changed in CodeMirror. Should look into it more. + const tokenProps = node.type.name.split('_'); + if (tokenProps) { + const props = new Set(tokenProps); + const isLink = props.has("hmd-internal-link"); + const isAlias = props.has("link-alias"); + const isPipe = props.has("link-alias-pipe"); + + const isMDUrl = props.has('url'); + + if (isLink && !isAlias && !isPipe || isMDUrl) { + let linkText = view.state.doc.sliceString(node.from, node.to); + + const indexOfHeaderMarker = linkText.indexOf('#'); + const indexOfHeaderMarkerInDoc = indexOfHeaderMarker + node.from; + if (indexOfHeaderMarkerInDoc >= node.from && indexOfHeaderMarkerInDoc <= node.to) { + if (!inLastPass(node.from - 2, location?.from)) { + + const fileName = linkText.substring(0, indexOfHeaderMarker); + const pattern = findPatternForFile(fileName, settings); + + // Overrides the heading character ('#') to what is specified in settings + if (pattern.headingVisual !== '') { + let overrideP2HWidget = Decoration.widget({ + widget: new CharacterOverwriteWidget(pattern.headingVisual), + }); + builder.add(indexOfHeaderMarkerInDoc, indexOfHeaderMarkerInDoc + 1, overrideP2HWidget); + } + + // Overrides the header separator character to what is specified in settings + if (pattern.headingSeparatorVisual !== '') { + const indexOfRangeMarker = linkText.indexOf(settings.headingSeparator) + node.from; + if (indexOfRangeMarker >= node.from && indexOfRangeMarker <= node.to) { + let overrideH2HWidget = Decoration.widget({ + widget: new CharacterOverwriteWidget(pattern.headingSeparatorVisual), + }); + builder.add(indexOfRangeMarker, indexOfRangeMarker + settings.headingSeparator.length, overrideH2HWidget); + } + } + } + + this.decoratedRanges.push({ + from: node.from - 2, + to: node.to + 2 + }); + } + } + } + } + }) + } + + const bufferedDecs = builder.finish(); + return bufferedDecs; + } + }, + { + decorations: v => v.decorations + } + ); + + return viewPlugin; +} \ No newline at end of file diff --git a/src/linkRangeView.ts b/src/livePreviewEmbedReplacer.ts similarity index 94% rename from src/linkRangeView.ts rename to src/livePreviewEmbedReplacer.ts index 12a58fc..ab159e5 100644 --- a/src/linkRangeView.ts +++ b/src/livePreviewEmbedReplacer.ts @@ -4,7 +4,7 @@ import { LinkRangeSettings } from "./settings"; import { RangeSetBuilder } from "@codemirror/state"; import { replaceEmbed } from "./embeds"; -export class LinkRangeView implements PluginValue { +export class LifePreviewEmbedReplacer implements PluginValue { decorations: DecorationSet = Decoration.none; settings: LinkRangeSettings; app: App; diff --git a/src/settings.ts b/src/settings.ts index b25d4ee..2806119 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,17 +1,37 @@ import LinkRange from "main"; -import { App, PluginSettingTab, Setting } from "obsidian"; +import { App, PluginSettingTab, Setting, ButtonComponent } from "obsidian"; import { postProcessorUpdate } from "./utils"; +export interface Pattern { + headingVisual: string; + headingSeparatorVisual: string; + path: string; +} + export interface LinkRangeSettings { headingSeparator: string; - altFormat: string; endInclusive: boolean; + altFormat: string; // This is for backwards compatibility + settingsVersion: string; + patterns: [Pattern] + getDefaultPattern() : Pattern } export const DEFAULT_SETTINGS: LinkRangeSettings = { headingSeparator: '..', - altFormat: '$note:$h1-$h2', - endInclusive: true + endInclusive: true, + altFormat: '', + settingsVersion: 'v2', + patterns: [{ headingVisual: '..', headingSeparatorVisual: '-', path: '/' }], + + getDefaultPattern() { + const first = this.patterns[0]; + if (!first) { + return { headingVisual: ':', headingSeparatorVisual: '-', path: '/' } + } + + return first; + }, } export class LinkRangeSettingTab extends PluginSettingTab { @@ -20,6 +40,33 @@ export class LinkRangeSettingTab extends PluginSettingTab { constructor(app: App, plugin: LinkRange) { super(app, plugin); this.plugin = plugin; + this.migrateOldSettings(); + } + + migrateOldSettings() { + const stgs = this.plugin.settings; + + const hasV1Settings = stgs.altFormat != undefined && stgs.altFormat.length > 0; + if (hasV1Settings) { + + // default altFormat string: `$note:$h1-$h2` + const altFormat = stgs.altFormat; + const indexOfNote = altFormat.indexOf('$note'); + const indexOfH1 = altFormat.indexOf('$h1'); + const indexOfH2 = altFormat.indexOf('$h2'); + + const formatIsValid = indexOfNote === 0 && indexOfH1 !== -1 && indexOfH2 !== -2; + + if (formatIsValid) { + const firstValue = altFormat.substring('$note'.length, indexOfH1); + const secondValue = altFormat.substring(indexOfH1 + '$h1'.length, indexOfH2); + + stgs.patterns = [{ headingVisual: firstValue, headingSeparatorVisual: secondValue, path: '' }] + } + + stgs.altFormat = ''; + this.plugin.saveSettings(); + } } display(): void { @@ -27,11 +74,11 @@ export class LinkRangeSettingTab extends PluginSettingTab { containerEl.empty(); - containerEl.createEl('h2', {text: 'Settings for link-range plugin'}); + this.createH2('Settings for link-range plugin') new Setting(containerEl) .setName('Heading Separator') - .setDesc('Defines the separator to be used to define a link heading range. Defaults to ".." (i.e. [[Note Name#h1..h2]])') + .setDesc('Defines the separator to be used to define a link heading range. Defaults to ".." (e.g. [[Note Name#h1..h2]])') .addText(text => text .setPlaceholder('Enter a separator string (defaults to ..)') .setValue(this.plugin.settings.headingSeparator) @@ -41,18 +88,6 @@ export class LinkRangeSettingTab extends PluginSettingTab { postProcessorUpdate(this.app) })); - new Setting(containerEl) - .setName('Alt Text Format') - .setDesc('Defines the alternate text format that gets shown in read mode. Use $note for the note placeholder and $h1/$h2 for the header placeholders') - .addText(text => text - .setPlaceholder('Enter an alt format') - .setValue(this.plugin.settings.altFormat) - .onChange(async (value) => { - this.plugin.settings.altFormat = value; - await this.plugin.saveSettings(); - postProcessorUpdate(this.app) - })); - new Setting(containerEl) .setName('End Inclusive') .setDesc('Whether or not the end heading should be inclusive or exclusive') @@ -63,5 +98,86 @@ export class LinkRangeSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); postProcessorUpdate(this.app) })); + + new Setting(this.containerEl) + .setName("Add a New Visual Pattern") + .setDesc("Add new pattern to match files in a directory. The first value will change the visual for the heading in a link. The second value will change the visual for separator. The third specifies the folder in which the files must be to match. The first match, starting bottom up, will be applied. Therefore, the first is the default pattern.") + .addButton((button: ButtonComponent) => { + button + .setTooltip("Add new pattern to match files in a directory.") + .setButtonText("+") + .setCta() + .onClick(() => { + this.plugin.settings.patterns.push({ + headingVisual: '', + headingSeparatorVisual: '', + path: '' + }); + this.plugin.saveSettings(); + this.display(); + }); + }); + + this.plugin.settings.patterns.forEach( + (pattern, index) => { + const s = new Setting(this.containerEl) + .addText(text => text + .setPlaceholder('Enter a heading override') + .setValue(pattern.headingVisual) + .onChange(async (value) => { + pattern.headingVisual = value; + await this.plugin.saveSettings(); + postProcessorUpdate(this.app) + })) + .addText(text => text + .setPlaceholder('Enter a separator override') + .setValue(pattern.headingSeparatorVisual) + .onChange(async (value) => { + pattern.headingSeparatorVisual = value; + await this.plugin.saveSettings(); + postProcessorUpdate(this.app) + })) + .addText(text => text + .setPlaceholder(index === 0 ? '(global)' : 'Enter a path') + .setValue(pattern.path) + .setDisabled(index === 0) + .onChange(async (value) => { + pattern.path = value; + await this.plugin.saveSettings(); + postProcessorUpdate(this.app) + })); + + if (index === 0) { + s.addExtraButton((cb) => { + cb.setIcon("lock") + .setTooltip("This pattern is the default and cannot be completed"); + }); + } + else { + if (index !== 0) { + s.addExtraButton((cb) => { + cb.setIcon("cross") + .setTooltip("Delete") + .onClick(() => { + this.plugin.settings.patterns.splice( + index, + 1 + ); + this.plugin.saveSettings(); + this.display(); + }); + + }); + } + } + } + ); } + + createH2(text: string) { + const {containerEl} = this; + containerEl.createEl('h2', { text: text }); + } + + } diff --git a/src/utils.ts b/src/utils.ts index 216e8bb..d72ce8a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ import { App, TFile } from "obsidian"; -import { LinkRangeSettings } from "./settings"; +import { LinkRangeSettings, Pattern } from "./settings"; export interface ParsedLink { note: string; @@ -26,11 +26,6 @@ export function checkLinkText(href: string, settings: LinkRangeSettings): Parsed const header = matches[2]; const split = header.split(settings.headingSeparator); - // our ranged link format is "#h1..h2" - if (split.length < 2) { - return null; - } - const note = matches[1]; const h1 = split[0]; const h2 = split[1]; @@ -41,9 +36,17 @@ export function checkLinkText(href: string, settings: LinkRangeSettings): Parsed altText = matches[3] } else { - altText = settings.altFormat.replace(NOTE_PLACEHOLDER, note) - altText = altText.replace(H1_PLACEHOLDER, h1) - altText = altText.replace(H2_PLACEHOLDER, h2) + let pattern = findPatternForFile(note, settings); + + let headingVisual = pattern.headingVisual === '' ? '#' : pattern.headingVisual; + let headingSeparatorVisual = pattern.headingSeparatorVisual === '' ? settings.headingSeparator : pattern.headingSeparatorVisual; + + if (h2 !== undefined) { + altText = `${note}${headingVisual}${h1}${headingSeparatorVisual}${h2}` + } + else { + altText = `${note}${headingVisual}${h1}` + } } return { note, h1, h2, altText } @@ -130,3 +133,14 @@ export function postProcessorUpdate(app: App) { app.workspace.updateOptions(); } + +export function findPatternForFile(fileName: string, settings: LinkRangeSettings) : Pattern { + const file = app.vault.getFiles().find((file) => file.basename === fileName); + + let pattern = [...settings.patterns].reverse().find((pattern: Pattern) => file?.path.startsWith(pattern.path)) + if (!pattern) { + pattern = settings.getDefaultPattern(); + } + + return pattern; +}