├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── esbuild.config.mjs ├── img ├── brc.png └── readme.png ├── manifest.json ├── package.json ├── src ├── indexer.ts ├── livePreview.ts ├── main.ts ├── process.ts ├── settings.ts ├── types.ts └── view.ts ├── styles.css ├── tsconfig.json └── versions.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaVersion": 12, 14 | "sourceType": "module" 15 | }, 16 | "plugins": [ 17 | "@typescript-eslint" 18 | ], 19 | "rules": { 20 | "indent": [ 21 | "error", 22 | 4 23 | ], 24 | "quotes": [ 25 | "error", 26 | "double" 27 | ], 28 | "semi": [ 29 | "error", 30 | "always" 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | *.iml 3 | .idea 4 | 5 | # npm 6 | node_modules 7 | package-lock.json 8 | 9 | # build 10 | main.js 11 | *.js.map 12 | 13 | # obsidian 14 | data.json 15 | 16 | .DS_Store 17 | *.zip 18 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": false, 4 | "semi": true 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "offref" 4 | ] 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This Plugin is no longer maintained. Please look at [Strange New Worlds](https://github.com/TfTHacker/obsidian42-strange-new-worlds) as an alternative. 2 | 3 | # Obsidian Block Reference Counts 4 | 5 | **By shabegom** 6 | 7 | **I would definitely check out 8 | [Strange New Worlds](https://github.com/TfTHacker/obsidian42-strange-new-worlds) 9 | instead of this plugin. Strange New Worlds seems a lot niced.** 10 | 11 | ## Known Issues 12 | 13 | There are some known problems with this plugin. I don't have time to address 14 | them. PRs welcome: 15 | 16 | - [#49](https://github.com/shabegom/obsidian-reference-count/issues/49) People 17 | report that this plugin causes lag in their vault. It seems to impact vaults 18 | with a large amount of references. There are a couple of settings to try and 19 | reduce the amount of indexing that happens. 20 | - [#66](https://github.com/shabegom/obsidian-reference-count/issues/66) There is 21 | a report that using Absolute Path links and this plugin causes renaming links 22 | to break in a pretty bad way. **If you use Absolute Path links I would not use 23 | this plugin at this time**. 24 | 25 | If you run into a bug, please submit an issue. If you have any questions reach 26 | out to @shabegom on the obsidian Discord. 27 | 28 | ![](img/readme.png) 29 | 30 | Show the amount of references you have in: 31 | 32 | - block references 33 | - headings 34 | - block reference links 35 | - embeds 36 | 37 | Click on the number to open a table with links to the note with the reference 38 | and the line the reference appears on. 39 | 40 | There are settings if you want counts to show up on parents, or children, or 41 | both. You can also choose to see a basic table, or a fancier search-like view of 42 | references. 43 | 44 | ## Changelog 45 | 46 | **0.3.0** Major Rewrite! 47 | 48 | - Uses new method of generated block references that is 10x faster than previous 49 | approach! 50 | - Should now support non-englished languages 51 | - Should support relative and full path links and embeds 52 | - Improved accuracy of the search results view 53 | - More responsive in updating reference counts 54 | 55 | **0.1.6** 56 | 57 | - Released in Community Plugins! 🎉 58 | 59 | **0.0.9** 60 | 61 | - Cleanup search view on unload 62 | - Hide the right sidebar tab 63 | 64 | **0.0.8** 65 | 66 | - New references view 67 | 68 | **0.0.6** 69 | 70 | - Initial release 71 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | 5 | const banner = `/* 6 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 7 | if you want to view the source, please visit the github repository of this plugin 8 | */ 9 | `; 10 | 11 | const prod = process.argv[2] === "production"; 12 | 13 | esbuild 14 | .build({ 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: ["./src/main.ts"], 19 | bundle: true, 20 | external: [ 21 | "obsidian", 22 | "electron", 23 | "@codemirror/autocomplete", 24 | "@codemirror/closebrackets", 25 | "@codemirror/collab", 26 | "@codemirror/commands", 27 | "@codemirror/comment", 28 | "@codemirror/fold", 29 | "@codemirror/gutter", 30 | "@codemirror/highlight", 31 | "@codemirror/history", 32 | "@codemirror/language", 33 | "@codemirror/lint", 34 | "@codemirror/matchbrackets", 35 | "@codemirror/panel", 36 | "@codemirror/rangeset", 37 | "@codemirror/rectangular-selection", 38 | "@codemirror/search", 39 | "@codemirror/state", 40 | "@codemirror/stream-parser", 41 | "@codemirror/text", 42 | "@codemirror/tooltip", 43 | "@codemirror/view", 44 | ...builtins, 45 | ], 46 | format: "cjs", 47 | watch: !prod, 48 | target: "es2016", 49 | logLevel: "info", 50 | sourcemap: prod ? false : "inline", 51 | treeShaking: true, 52 | outfile: "main.js", 53 | }) 54 | .catch(() => process.exit(1)); 55 | -------------------------------------------------------------------------------- /img/brc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shabegom/obsidian-reference-count/9dfafea25c6a13df309dce68d84020de63da50e1/img/brc.png -------------------------------------------------------------------------------- /img/readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shabegom/obsidian-reference-count/9dfafea25c6a13df309dce68d84020de63da50e1/img/readme.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "block-reference-count", 3 | "name": "Block Reference Counter", 4 | "version": "0.4.3", 5 | "minAppVersion": "0.12.3", 6 | "description": "Shows the count of block references next to the block-id", 7 | "author": "shabegom", 8 | "authorUrl": "https://shbgm.ca", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "block-ref-counts", 3 | "version": "0.4.0", 4 | "description": "count block references in obsidian vault", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json", 10 | "cp": "cp ./main.js ~/plugin-development/.obsidian/plugins/ref-count/" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@rollup/plugin-commonjs": "^18.0.0", 17 | "@rollup/plugin-node-resolve": "^11.2.1", 18 | "@rollup/plugin-typescript": "^8.2.1", 19 | "@types/node": "^14.14.37", 20 | "@types/workerpool": "^6.1.0", 21 | "@typescript-eslint/eslint-plugin": "^4.24.0", 22 | "@typescript-eslint/parser": "^4.24.0", 23 | "builtin-modules": "^3.2.0", 24 | "esbuild": "0.13.12", 25 | "eslint": "^7.27.0", 26 | "obsidian": "^0.13.21", 27 | "rollup": "^2.32.1", 28 | "rollup-plugin-web-worker-loader": "^1.6.1", 29 | "tslib": "^2.2.0", 30 | "typescript": "^4.2.4" 31 | }, 32 | "dependencies": { 33 | "@codemirror/language": "^0.19.7" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/indexer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | CachedMetadata, 4 | HeadingCache, 5 | stripHeading, 6 | TFile, 7 | Pos, 8 | } from "obsidian"; 9 | import { Link, ListItem, Section, TransformedCache } from "./types"; 10 | 11 | let references: { [x: string]: Link[] }; 12 | 13 | export function buildLinksAndReferences(app: App): void { 14 | const refs = app.fileManager 15 | .getAllLinkResolutions() 16 | .reduce( 17 | ( 18 | acc: { [x: string]: Link[] }, 19 | link: Link 20 | ): { [x: string]: Link[] } => { 21 | let key = link.reference.link; 22 | if (key.includes("/")) { 23 | const keyArr = key.split("/"); 24 | key = keyArr[keyArr.length - 1]; 25 | } 26 | if (!acc[key]) { 27 | acc[key] = []; 28 | } 29 | if (acc[key]) { 30 | acc[key].push(link); 31 | } 32 | return acc; 33 | }, 34 | {} 35 | ); 36 | const allLinks = Object.entries(app.metadataCache.getLinks()).reduce( 37 | (acc, [key, links]) => { 38 | links.forEach( 39 | (link: { 40 | link: string; 41 | original: string; 42 | position: Pos; 43 | }): void => { 44 | if ( 45 | link.original.startsWith("[[#") || 46 | link.original.startsWith("![[#") 47 | ) { 48 | const newLink: Link = { 49 | reference: { 50 | link: link.link, 51 | displayText: link.link, 52 | position: link.position, 53 | }, 54 | resolvedFile: app.vault.getAbstractFileByPath( 55 | key 56 | ) as TFile, 57 | resolvedPaths: [link.link], 58 | sourceFile: app.vault.getAbstractFileByPath( 59 | key 60 | ) as TFile, 61 | }; 62 | acc.push(newLink); 63 | } 64 | } 65 | ); 66 | return acc; 67 | }, 68 | [] 69 | ); 70 | allLinks.forEach((link: Link) => { 71 | if (link.sourceFile) { 72 | const key = `${link.sourceFile.basename}${link.reference.link}`; 73 | if (!refs[key]) { 74 | refs[key] = []; 75 | } 76 | if (refs[key]) { 77 | refs[key].push(link); 78 | } 79 | } 80 | }); 81 | references = refs; 82 | } 83 | 84 | export function getCurrentPage({ 85 | file, 86 | app, 87 | }: { 88 | file: TFile; 89 | app: App; 90 | }): TransformedCache { 91 | const cache = app.metadataCache.getFileCache(file); 92 | if (!references) { 93 | buildLinksAndReferences(app); 94 | } 95 | const headings: string[] = Object.values( 96 | app.metadataCache.metadataCache 97 | ).reduce((acc: string[], file: CachedMetadata) => { 98 | const headings = file.headings; 99 | if (headings) { 100 | headings.forEach((heading: HeadingCache) => { 101 | acc.push(heading.heading); 102 | }); 103 | } 104 | return acc; 105 | }, []); 106 | const transformedCache: TransformedCache = {}; 107 | if (cache.blocks) { 108 | transformedCache.blocks = Object.values(cache.blocks).map((block) => ({ 109 | key: block.id, 110 | pos: block.position, 111 | page: file.basename, 112 | type: "block", 113 | references: references[`${file.basename}#^${block.id}`] || [], 114 | })); 115 | } 116 | if (cache.headings) { 117 | transformedCache.headings = cache.headings.map( 118 | (header: { 119 | heading: string; 120 | position: Pos; 121 | }) => ({ 122 | original: header.heading, 123 | key: stripHeading(header.heading), 124 | pos: header.position, 125 | 126 | page: file.basename, 127 | type: "header", 128 | references: 129 | references[ 130 | `${file.basename}#${stripHeading(header.heading)}` 131 | ] || [], 132 | }) 133 | ); 134 | } 135 | if (cache.sections) { 136 | transformedCache.sections = createListSections( 137 | cache 138 | ); 139 | } 140 | if (cache.links) { 141 | transformedCache.links = cache.links.map((link) => { 142 | if (link.link.includes("/")) { 143 | const keyArr = link.link.split("/"); 144 | link.link = keyArr[keyArr.length - 1]; 145 | } 146 | return { 147 | key: link.link, 148 | type: "link", 149 | pos: link.position, 150 | page: file.basename, 151 | references: references[link.link] || [], 152 | }; 153 | }); 154 | if (transformedCache.links) { 155 | transformedCache.links = transformedCache.links.map((link) => { 156 | if (link.key.includes("/")) { 157 | const keyArr = link.key.split("/"); 158 | link.key = keyArr[keyArr.length - 1]; 159 | } 160 | if (link.key.includes("#") && !link.key.includes("#^")) { 161 | const heading = headings.filter( 162 | (heading: string) => 163 | stripHeading(heading) === link.key.split("#")[1] 164 | )[0]; 165 | link.original = heading ? heading : undefined; 166 | } 167 | if (link.key.startsWith("#^") || link.key.startsWith("#")) { 168 | link.key = `${link.page}${link.key}`; 169 | link.references = references[link.key] || []; 170 | } 171 | return link; 172 | }); 173 | } 174 | } 175 | 176 | if (cache.embeds) { 177 | transformedCache.embeds = cache.embeds.map((embed) => { 178 | if (embed.link.includes("/")) { 179 | const keyArr = embed.link.split("/"); 180 | embed.link = keyArr[keyArr.length - 1]; 181 | } 182 | return { 183 | key: embed.link, 184 | page: file.basename, 185 | type: "link", 186 | pos: embed.position, 187 | references: references[embed.link] || [], 188 | }; 189 | }); 190 | if (transformedCache.embeds) { 191 | transformedCache.embeds = transformedCache.embeds.map((embed) => { 192 | if ( 193 | embed.key.includes("#") && 194 | !embed.key.includes("#^") && 195 | transformedCache.headings 196 | ) { 197 | const heading = headings.filter((heading: string) => 198 | heading.includes(embed.key.split("#")[1]) 199 | )[0]; 200 | 201 | embed.original = heading ? heading : undefined; 202 | } 203 | 204 | if (embed.key.startsWith("#^") || embed.key.startsWith("#")) { 205 | embed.key = `${file.basename}${embed.key}`; 206 | embed.references = references[embed.key] || []; 207 | } 208 | return embed; 209 | }); 210 | } 211 | } 212 | return transformedCache; 213 | } 214 | 215 | /** 216 | * If the section is of type list, add the list items from the metadataCache to the section object. 217 | * This makes it easier to iterate a list when building block ref buttons 218 | * 219 | * @param {SectionCache[]} sections 220 | * @param {ListItemCache[]} listItems 221 | * 222 | * @return {Section[]} Array of sections with additional items key 223 | */ 224 | 225 | function createListSections( 226 | cache: CachedMetadata 227 | ): Section[] { 228 | if (cache.listItems) { 229 | return cache.sections.map((section) => { 230 | const items: ListItem[] = []; 231 | if (section.type === "list") { 232 | cache.listItems.forEach((item: ListItem) => { 233 | if ( 234 | item.position.start.line >= 235 | section.position.start.line && 236 | item.position.start.line <= section.position.end.line 237 | ) { 238 | const id = cache.embeds?.find( 239 | (embed) => 240 | embed.position.start.line === 241 | item.position.start.line 242 | )?.link || cache.links?.find( 243 | (link) => 244 | link.position.start.line === 245 | item.position.start.line 246 | )?.link || ""; 247 | items.push({ key: id, pos: item.position, ...item }); 248 | } 249 | }); 250 | const sectionWithItems = { items, ...section }; 251 | return sectionWithItems; 252 | } 253 | return section; 254 | }); 255 | } 256 | 257 | return cache.sections; 258 | } 259 | -------------------------------------------------------------------------------- /src/livePreview.ts: -------------------------------------------------------------------------------- 1 | import { App } from "obsidian"; 2 | import { 3 | EditorView, 4 | WidgetType, 5 | Decoration, 6 | ViewUpdate, 7 | ViewPlugin, 8 | DecorationSet, 9 | PluginValue, 10 | Range, 11 | } from "@codemirror/view"; 12 | import { StateField, StateEffect } from "@codemirror/state"; 13 | import { TransformedCachedItem } from "./types"; 14 | import { createCounter } from "./view"; 15 | import BlockRefCounter from "./main"; 16 | import { createRefTableElement, createSearchElement } from "./view"; 17 | import { getSettings } from "./settings"; 18 | 19 | class ButtonWidget extends WidgetType { 20 | constructor( 21 | public readonly count: number, 22 | public readonly block: TransformedCachedItem 23 | ) { 24 | super(); 25 | } 26 | 27 | toDOM() { 28 | const countEl = createCounter(this.block, this.count); 29 | return countEl; 30 | } 31 | 32 | ignoreEvent() { 33 | return false; 34 | } 35 | } 36 | 37 | function buttons(plugin: BlockRefCounter, view: EditorView) { 38 | const buttons = plugin.createPreview(plugin); 39 | const note = plugin.app.workspace.getActiveFile().basename; 40 | let widgets: Range[] = []; 41 | for (const { from, to } of view.visibleRanges) { 42 | for (const button of buttons) { 43 | if ( 44 | button.block.pos.start.offset >= from && 45 | button.block.pos.end.offset <= to && 46 | button.block.references && 47 | button.block.references.length > 0 && 48 | button.block.page === note 49 | ) { 50 | const deco = Decoration.widget({ 51 | widget: new ButtonWidget( 52 | button.block?.references.length, 53 | button.block 54 | ), 55 | }); 56 | widgets.push(deco.range(button.block.pos.end.offset)); 57 | } 58 | } 59 | } 60 | widgets = widgets.sort((a, b) => a.from - b.from).reduce((acc, widget) => { 61 | if (acc.length === 0) { 62 | return [widget]; 63 | } 64 | const last = acc[acc.length - 1]; 65 | if (last.from === widget.from) { 66 | return acc; 67 | } 68 | return [...acc, widget]; 69 | }, []); 70 | return Decoration.set(widgets); 71 | } 72 | 73 | export function blockRefCounterPlugin( 74 | plugin: BlockRefCounter 75 | ): ViewPlugin { 76 | return ViewPlugin.fromClass( 77 | class { 78 | decorations: DecorationSet; 79 | effects: StateEffect[] = []; 80 | 81 | constructor(view: EditorView) { 82 | this.decorations = buttons(plugin, view); 83 | } 84 | 85 | update(update: ViewUpdate) { 86 | if (update.docChanged || update.viewportChanged) { 87 | this.decorations = buttons(plugin, update.view); 88 | } 89 | } 90 | }, 91 | { 92 | decorations: (v) => v.decorations, 93 | eventHandlers: { 94 | mousedown: (e, view) => { 95 | const target = e.target as HTMLElement; 96 | if (target.classList.contains("block-ref-count")) { 97 | const id = target.dataset.blockRefId; 98 | const block = plugin.buttons.filter( 99 | (button) => button.block.key === id 100 | )[0].block; 101 | const pos = view.posAtDOM(target); 102 | const effects: StateEffect[] = [ 103 | addReferences.of({ 104 | to: pos, 105 | app: plugin.app, 106 | block, 107 | }), 108 | ]; 109 | if (!view.state.field(referencesField, false)) { 110 | effects.push( 111 | StateEffect.appendConfig.of(referencesField) 112 | ); 113 | } 114 | view.dispatch({ effects }); 115 | } 116 | }, 117 | }, 118 | } 119 | ); 120 | } 121 | 122 | class referencesWidget extends WidgetType { 123 | constructor(public app: App, public block: TransformedCachedItem) { 124 | super(); 125 | } 126 | 127 | toDOM() { 128 | const val = document.createElement("div"); 129 | const { tableType } = getSettings(); 130 | if (tableType === "basic") { 131 | createRefTableElement(this.app, this.block, val); 132 | } 133 | if (tableType === "search") { 134 | createSearchElement(this.app, this.block, val); 135 | } 136 | return val; 137 | } 138 | } 139 | 140 | const referencesDecoration = (app: App, block: TransformedCachedItem) => { 141 | return Decoration.widget({ 142 | widget: new referencesWidget(app, block), 143 | side: 2, 144 | block: true, 145 | }); 146 | }; 147 | 148 | const addReferences = StateEffect.define<{ 149 | to: number; 150 | app: App; 151 | block: TransformedCachedItem; 152 | }>(); 153 | 154 | export const referencesField = StateField.define({ 155 | create() { 156 | return Decoration.none; 157 | }, 158 | update(references, tr) { 159 | let exists = false; 160 | references = references.map(tr.changes); 161 | for (const e of tr.effects) 162 | if (e.is(addReferences)) { 163 | references = references.update({ 164 | filter: (_from, to) => { 165 | if (to === e.value.to) { 166 | exists = true; 167 | } 168 | return to !== e.value.to; 169 | }, 170 | }); 171 | if (!exists) { 172 | references = references.update({ 173 | add: [ 174 | referencesDecoration( 175 | e.value.app, 176 | e.value.block 177 | ).range(e.value.to), 178 | ], 179 | }); 180 | } 181 | } 182 | 183 | return references; 184 | }, 185 | provide: (f) => EditorView.decorations.from(f), 186 | }); 187 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | EventRef, 4 | MarkdownView, 5 | debounce, 6 | Constructor, 7 | Plugin, 8 | WorkspaceLeaf, 9 | View, 10 | } from "obsidian"; 11 | import { TransformedCache, TransformedCachedItem } from "./types"; 12 | import { buildLinksAndReferences, getCurrentPage } from "./indexer"; 13 | import { 14 | BlockRefCountSettingTab, 15 | BlockRefCountSettings, 16 | getSettings, 17 | updateSettings, 18 | } from "./settings"; 19 | import { createPreviewView, processPage } from "./process"; 20 | import { blockRefCounterPlugin, referencesField } from "./livePreview"; 21 | 22 | /* 23 | * BlockRefCounter Plugin 24 | * by shabegom 25 | * 26 | * Iterates through the cache of all notes in a vault and creates an index of block-ids, headings, links referencing a block-id or heading, and embeds 27 | * Adds a button in Preview view with the count of references found 28 | * When button is clicked, reveals a table with links to each reference and line reference exists on 29 | */ 30 | export default class BlockRefCounter extends Plugin { 31 | public resolved: EventRef; 32 | public page: TransformedCache; 33 | public buttons: { 34 | block?: TransformedCachedItem; 35 | val?: HTMLElement; 36 | }[] = []; 37 | public createPreview = createPreviewView; 38 | public settings: BlockRefCountSettings; 39 | 40 | async onload(): Promise { 41 | console.log("loading plugin: Block Reference Counter"); 42 | await this.loadSettings(); 43 | this.settings = getSettings(); 44 | 45 | this.addSettingTab(new BlockRefCountSettingTab(this.app, this)); 46 | 47 | const indexDebounce = debounce( 48 | () => { 49 | buildLinksAndReferences(this.app); 50 | }, 51 | 3000, 52 | true 53 | ); 54 | const previewDebounce = debounce( 55 | () => { 56 | this.buttons = []; 57 | this.buttons = createPreviewView(this); 58 | }, 59 | 500, 60 | true 61 | ); 62 | 63 | /** 64 | * Fire the initial indexing only if layoutReady = true 65 | * and if the metadataCache has been resolved for the first time 66 | * avoids trying to create an index while obsidian is indexing files 67 | */ 68 | this.app.workspace.onLayoutReady(() => { 69 | unloadSearchViews(this.app); 70 | const resolved = this.app.metadataCache.on("resolved", () => { 71 | this.app.metadataCache.offref(resolved); 72 | if (this.settings.indexOnVaultOpen) { 73 | buildLinksAndReferences(this.app); 74 | this.buttons = createPreviewView(this); 75 | const activeView = this.app.workspace.getActiveViewOfType( 76 | MarkdownView as unknown as Constructor 77 | ); 78 | if (activeView) { 79 | const file = activeView.file; 80 | this.page = getCurrentPage({ file, app: this.app }); 81 | } 82 | } 83 | this.registerEditorExtension([ 84 | blockRefCounterPlugin(this), 85 | referencesField, 86 | ]); 87 | }); 88 | }); 89 | 90 | this.registerView("search-ref", (leaf: WorkspaceLeaf) => { 91 | if (!this.app.viewRegistry.getViewCreatorByType("search")) { 92 | return; 93 | } 94 | const newView: View = 95 | this.app.viewRegistry.getViewCreatorByType("search")(leaf); 96 | newView.getViewType = () => "search-ref"; 97 | return newView; 98 | }); 99 | 100 | // * 101 | // Event listeners to re-index notes if the cache changes or a note is deleted 102 | // triggers creation of block ref buttons on the preview view 103 | 104 | this.registerEvent( 105 | this.app.vault.on("delete", () => { 106 | indexDebounce(); 107 | }) 108 | ); 109 | 110 | 111 | 112 | // * 113 | // Event listeners for layout changes to update the preview view with a block ref count button 114 | // 115 | this.registerEvent( 116 | this.app.workspace.on("layout-change", () => { 117 | if (this.settings.indexOnLayoutChange) { 118 | indexDebounce(); 119 | previewDebounce(); 120 | } 121 | }) 122 | ); 123 | 124 | this.registerEvent( 125 | this.app.workspace.on("file-open", (file): void => { 126 | if (this.settings.indexOnFileOpen) { 127 | indexDebounce(); 128 | this.page = getCurrentPage({ file, app: this.app }); 129 | previewDebounce(); 130 | } 131 | }) 132 | ); 133 | 134 | this.registerEvent( 135 | this.app.metadataCache.on("resolve", (file) => { 136 | if (this.settings.indexOnFileChange) { 137 | indexDebounce(); 138 | this.page = getCurrentPage({ file, app: this.app }); 139 | previewDebounce(); 140 | } 141 | }) 142 | ); 143 | 144 | this.registerMarkdownPostProcessor((el, ctx) => { 145 | const sectionInfo = ctx.getSectionInfo(el); 146 | const lineStart = sectionInfo && sectionInfo.lineStart; 147 | if (this.page && lineStart) { 148 | const processed = processPage( 149 | this.page, 150 | this.app, 151 | el, 152 | lineStart 153 | ); 154 | if (processed.length > 0) { 155 | const ids = this.buttons.map((button) => button.block.key); 156 | processed.forEach((item) => { 157 | if (!ids.includes(item.block.key)) { 158 | this.buttons.push(item); 159 | } 160 | }); 161 | } 162 | } 163 | }); 164 | } 165 | 166 | onunload(): void { 167 | console.log("unloading plugin: Block Reference Counter"); 168 | unloadButtons(this.app); 169 | unloadSearchViews(this.app); 170 | } 171 | async loadSettings(): Promise { 172 | const newSettings = await this.loadData(); 173 | updateSettings(newSettings); 174 | } 175 | async saveSettings(): Promise { 176 | await this.saveData(getSettings()); 177 | } 178 | } 179 | 180 | /** 181 | * if there are block reference buttons in the current view, remove them 182 | * used when the plugin is unloaded 183 | * 184 | * @param {App} app 185 | * 186 | * @return {void} 187 | */ 188 | function unloadButtons(app: App): void { 189 | let buttons; 190 | const activeLeaf = app.workspace.getActiveViewOfType( 191 | MarkdownView as unknown as Constructor 192 | ); 193 | if (activeLeaf) { 194 | buttons = activeLeaf.containerEl.querySelectorAll("#count"); 195 | } 196 | buttons && buttons.forEach((button: HTMLElement) => button.remove()); 197 | } 198 | 199 | function unloadSearchViews(app: App): void { 200 | app.workspace 201 | .getLeavesOfType("search-ref") 202 | .forEach((leaf: WorkspaceLeaf) => leaf.detach()); 203 | } 204 | -------------------------------------------------------------------------------- /src/process.ts: -------------------------------------------------------------------------------- 1 | import { App, View, Constructor, MarkdownView } from "obsidian"; 2 | import { TransformedCache, Section, TransformedCachedItem } from "./types"; 3 | import { getCurrentPage } from "./indexer"; 4 | import { getSettings } from "./settings"; 5 | import { createButtonElements } from "./view"; 6 | import BlockRefCounter from "./main"; 7 | /** 8 | * Finds the sections present in a note's Preview, iterates them and adds references if required 9 | * This duplicates some of the functionality of onMarkdownPostProcessor, but is fired on layout and leaf changes 10 | * @param {App} app 11 | * @return {void} 12 | */ 13 | 14 | export function createPreviewView( 15 | plugin: BlockRefCounter 16 | ): { block?: TransformedCachedItem; val?: HTMLElement }[] { 17 | const buttons: { block?: TransformedCachedItem; val?: HTMLElement }[] = []; 18 | const app = plugin.app; 19 | const activeView = app.workspace.getActiveViewOfType( 20 | MarkdownView as unknown as Constructor 21 | ); 22 | if (activeView) { 23 | const page = getCurrentPage({ file: activeView.file, app }); 24 | try { 25 | activeView.previewMode?.renderer.onRendered(() => { 26 | // if previewMode exists and has sections, get the sections 27 | const elements = activeView.previewMode?.renderer?.sections; 28 | if (page && elements) { 29 | elements.forEach( 30 | (section: { el: HTMLElement; lineStart: number }) => { 31 | const processed = processPage( 32 | page, 33 | app, 34 | section.el, 35 | section.lineStart 36 | ); 37 | if (processed.length > 0) { 38 | buttons.push(...processed); 39 | } 40 | } 41 | ); 42 | } 43 | }); 44 | // if previewMode doesn't exist or has no sections, get the whole page 45 | const el = document.createElement("div"); 46 | const cache = plugin.app.metadataCache.getFileCache( 47 | activeView.file 48 | ); 49 | if (cache) { 50 | const { sections } = cache; 51 | if (sections) { 52 | sections.forEach((section) => { 53 | const processed = processPage( 54 | page, 55 | app, 56 | el, 57 | section.position.start.line 58 | ); 59 | if (processed.length > 0) { 60 | buttons.push(...processed); 61 | } 62 | }); 63 | } 64 | } 65 | 66 | return buttons; 67 | } catch (e) { 68 | console.log(e); 69 | } 70 | } 71 | } 72 | 73 | export function processPage( 74 | page: TransformedCache, 75 | app: App, 76 | el: HTMLElement, 77 | start: number 78 | ): { block?: TransformedCachedItem; val?: HTMLElement }[] { 79 | const buttons: { block?: TransformedCachedItem; val?: HTMLElement }[] = []; 80 | const settings = getSettings(); 81 | if (page.sections) { 82 | page.sections.forEach((pageSection: Section) => { 83 | if (pageSection.position.start.line === start) { 84 | pageSection.pos = pageSection.position.start.line; 85 | const type = pageSection?.type; 86 | 87 | // find embeds because their section.type is paragraph but they need to be processed differently 88 | const embeds = el.querySelectorAll(".internal-embed"); 89 | const hasEmbed = embeds.length > 0 ? true : false; 90 | if ( 91 | settings.displayParent && 92 | settings.displayBlocks && 93 | page.blocks && 94 | !hasEmbed && 95 | (type === "paragraph" || 96 | type === "list" || 97 | type === "blockquote" || 98 | type === "code") 99 | ) { 100 | const blockButtons = addBlockReferences( 101 | el, 102 | page.blocks, 103 | pageSection 104 | ); 105 | buttons.push(...blockButtons); 106 | } 107 | if ( 108 | settings.displayParent && 109 | settings.displayHeadings && 110 | page.headings && 111 | type === "heading" 112 | ) { 113 | const headerButtons = addHeaderReferences( 114 | el, 115 | page.headings, 116 | pageSection 117 | ); 118 | buttons.push(...headerButtons); 119 | } 120 | if ( 121 | settings.displayChild && 122 | settings.displayLinks && 123 | page.links 124 | ) { 125 | const linkButtons = addLinkReferences( 126 | el, 127 | page.links, 128 | pageSection 129 | ); 130 | buttons.push(...linkButtons); 131 | } 132 | if ( 133 | settings.displayChild && 134 | settings.displayEmbeds && 135 | page.embeds 136 | ) { 137 | const embedButtons = addEmbedReferences( 138 | el, 139 | page.embeds, 140 | pageSection 141 | ); 142 | buttons.push(...embedButtons); 143 | } 144 | } 145 | }); 146 | if (buttons.length > 0) { 147 | createButtonElements(app, buttons); 148 | } 149 | return buttons; 150 | } 151 | } 152 | 153 | /** 154 | * Iterate through the blocks in the note and add a block ref button if the section includes a block-id 155 | * 156 | * 157 | * @param {App} app 158 | * @param {HTMLElement} val the HTMLElement to attach the button to 159 | * @param {Block[]} blocks Array of blocks from pages index 160 | * @param {Section} section Section object from pages index 161 | * 162 | * @return {void} 163 | */ 164 | function addBlockReferences( 165 | val: HTMLElement, 166 | blocks: TransformedCache["headings"], 167 | section: Section 168 | ): { block?: TransformedCachedItem; val?: HTMLElement }[] { 169 | const blockButtons: { block: TransformedCachedItem; val: HTMLElement }[] = 170 | []; 171 | if (section.id || section.items) { 172 | blocks && 173 | blocks.forEach((block) => { 174 | if (block.key === section.id) { 175 | if (section.type === "paragraph") { 176 | blockButtons.push({ block, val }); 177 | } 178 | 179 | if ( 180 | section.type === "blockquote" || 181 | section.type === "code" 182 | ) { 183 | blockButtons.push({ block, val }); 184 | } 185 | } 186 | 187 | // Iterate each list item and add the button to items with block-ids 188 | 189 | if (section.type === "list") { 190 | section.items.forEach((item) => { 191 | if (item.id === block.key) { 192 | block.type = "block-list"; 193 | blockButtons.push({ 194 | block, 195 | val: document.createElement("div"), 196 | }); 197 | } 198 | }); 199 | } 200 | }); 201 | } 202 | return blockButtons; 203 | } 204 | 205 | function addEmbedReferences( 206 | val: HTMLElement, 207 | embeds: TransformedCache["embeds"], 208 | section: Section 209 | ): { block?: TransformedCachedItem; val?: HTMLElement }[] { 210 | const embedButtons: { block: TransformedCachedItem; val: HTMLElement }[] = 211 | []; 212 | embeds.forEach((embed) => { 213 | if (section.pos === embed.pos.start.line) { 214 | if (section.type === "paragraph") { 215 | embedButtons.push({ block: embed, val }); 216 | } 217 | 218 | if (section.type === "blockquote" || section.type === "code") { 219 | embedButtons.push({ block: embed, val }); 220 | } 221 | } 222 | 223 | // Iterate each list item and add the button to items with block-ids 224 | 225 | if (section.type === "list") { 226 | section.items.forEach((item) => { 227 | if ( 228 | item.key === embed.key && 229 | item.position.start.line === embed.pos.start.line 230 | ) { 231 | embed.type = "link-list"; 232 | embedButtons.push({ 233 | block: embed, 234 | val: document.createElement("div"), 235 | }); 236 | } 237 | }); 238 | } 239 | }); 240 | return embedButtons; 241 | } 242 | 243 | /** 244 | * Iterate through links (includes transcluded embeds) and add a block ref button if the link has an associated block ref 245 | * 246 | * @param {App} app 247 | * @param {HTMLElement} val HTMLElement to attach the button to 248 | * @param {EmbedOrLinkItem[]} links Array of links and embeds from pages index 249 | * @param {Section} section Section object from pages index 250 | * 251 | * @return {void} 252 | */ 253 | function addLinkReferences( 254 | val: HTMLElement, 255 | links: TransformedCachedItem[], 256 | section: Section 257 | ): { block?: TransformedCachedItem; val?: HTMLElement }[] { 258 | const linkButtons: { block?: TransformedCachedItem; val: HTMLElement }[] = 259 | []; 260 | links.forEach((link) => { 261 | if ( 262 | section.type === "paragraph" && 263 | section.pos === link.pos.start.line 264 | ) { 265 | linkButtons.push({ block: link, val }); 266 | } 267 | // Have to iterate list items so the button gets attached to the right element 268 | if (section.type === "list") { 269 | section.items.forEach((item, index: number) => { 270 | const buttons = val.querySelectorAll("li"); 271 | if (item.pos === link.pos.start.line) { 272 | link.type = "link-list"; 273 | linkButtons.push({ block: link, val: buttons[index] }); 274 | } 275 | }); 276 | } 277 | }); 278 | return linkButtons; 279 | } 280 | 281 | /** 282 | * Adds a block ref button to each header that has an associated header link or embed 283 | * 284 | * @param {App} app 285 | * @param {HTMLElement} val HTMLElement to attach the button to 286 | * @param {Heading[]} headings Array of heading objects from pages index 287 | * @param {Section} section Section object from pages index 288 | * 289 | * @return {void} 290 | */ 291 | 292 | function addHeaderReferences( 293 | val: HTMLElement, 294 | headings: TransformedCachedItem[], 295 | section: Section 296 | ): { block?: TransformedCachedItem; val: HTMLElement }[] { 297 | const headerButtons: { block?: TransformedCachedItem; val: HTMLElement }[] = 298 | []; 299 | if (headings) { 300 | headings.forEach((header: TransformedCachedItem) => { 301 | header.pos.start.line === section.pos && 302 | headerButtons.push({ block: header, val }); 303 | }); 304 | } 305 | return headerButtons; 306 | } 307 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import { App, Setting, PluginSettingTab } from "obsidian"; 2 | import BlockRefCounter from "./main"; 3 | 4 | export interface BlockRefCountSettings { 5 | displayParent: boolean; 6 | displayChild: boolean; 7 | displayBlocks: boolean; 8 | displayHeadings: boolean; 9 | displayLinks: boolean; 10 | displayEmbeds: boolean; 11 | tableType: string; 12 | indexOnVaultOpen: boolean; 13 | indexOnFileOpen: boolean; 14 | indexOnFileChange: boolean; 15 | indexOnLayoutChange: boolean; 16 | } 17 | 18 | export const DEFAULT_SETTINGS: BlockRefCountSettings = { 19 | displayParent: true, 20 | displayChild: true, 21 | displayBlocks: true, 22 | displayHeadings: true, 23 | displayLinks: true, 24 | displayEmbeds: true, 25 | indexOnVaultOpen: true, 26 | indexOnFileOpen: true, 27 | indexOnFileChange: true, 28 | indexOnLayoutChange: true, 29 | tableType: "search", 30 | }; 31 | 32 | let settings: BlockRefCountSettings = { ...DEFAULT_SETTINGS }; 33 | 34 | export const getSettings = (): BlockRefCountSettings => { 35 | return { ...settings }; 36 | }; 37 | 38 | export const updateSettings = ( 39 | newSettings: Partial 40 | ): BlockRefCountSettings => { 41 | settings = { ...settings, ...newSettings }; 42 | 43 | return getSettings(); 44 | }; 45 | 46 | export class BlockRefCountSettingTab extends PluginSettingTab { 47 | plugin: BlockRefCounter; 48 | 49 | constructor(app: App, plugin: BlockRefCounter) { 50 | super(app, plugin); 51 | this.plugin = plugin; 52 | } 53 | 54 | display(): void { 55 | const { containerEl } = this; 56 | 57 | containerEl.empty(); 58 | 59 | containerEl.createEl("h2", { 60 | text: "Block Reference Counter Settings", 61 | }); 62 | 63 | 64 | containerEl.createEl("h3", { 65 | text: "What elements should references be displayed on?", 66 | }); 67 | 68 | new Setting(containerEl) 69 | .setName("Display on Parents") 70 | .setDesc( 71 | "Display the count of block references on the parent block or header" 72 | ) 73 | .addToggle((toggle) => { 74 | toggle.setValue(getSettings().displayParent); 75 | toggle.onChange(async (val) => { 76 | updateSettings({ displayParent: val }); 77 | await this.plugin.saveSettings(); 78 | }); 79 | }); 80 | new Setting(containerEl) 81 | .setName("Display on Children") 82 | .setDesc( 83 | "Display the count of block references on the child reference blocks" 84 | ) 85 | .addToggle((toggle) => { 86 | toggle.setValue(getSettings().displayChild); 87 | toggle.onChange(async (val) => { 88 | updateSettings({ displayChild: val }); 89 | await this.plugin.saveSettings(); 90 | }); 91 | }); 92 | 93 | new Setting(containerEl) 94 | .setName("Display on Blocks") 95 | .setDesc("Display the count of block references on blocks") 96 | .addToggle((toggle) => { 97 | toggle.setValue(getSettings().displayBlocks); 98 | toggle.onChange(async (val) => { 99 | updateSettings({ displayBlocks: val }); 100 | await this.plugin.saveSettings(); 101 | }); 102 | }); 103 | new Setting(containerEl) 104 | .setName("Display on Headers") 105 | .setDesc("Display the count of block references on headers") 106 | .addToggle((toggle) => { 107 | toggle.setValue(getSettings().displayHeadings); 108 | toggle.onChange(async (val) => { 109 | updateSettings({ displayHeadings: val }); 110 | await this.plugin.saveSettings(); 111 | }); 112 | }); 113 | new Setting(containerEl) 114 | .setName("Display on Links") 115 | .setDesc("Display the count of block references on links") 116 | .addToggle((toggle) => { 117 | toggle.setValue(getSettings().displayLinks); 118 | toggle.onChange(async (val) => { 119 | updateSettings({ displayLinks: val }); 120 | await this.plugin.saveSettings(); 121 | }); 122 | }); 123 | new Setting(containerEl) 124 | .setName("Display on Embeds") 125 | .setDesc("Display the count of block references on Embeds") 126 | .addToggle((toggle) => { 127 | toggle.setValue(getSettings().displayEmbeds); 128 | toggle.onChange(async (val) => { 129 | updateSettings({ displayEmbeds: val }); 130 | await this.plugin.saveSettings(); 131 | }); 132 | }); 133 | 134 | containerEl.createEl("h3", { 135 | text: "When should new references be indexed?", 136 | }); 137 | containerEl.createEl("p", { 138 | text: "If you are experieincing lag, try toggling these settings off. Reload your vault for these settings to apply", 139 | }); 140 | 141 | new Setting(containerEl) 142 | .setName("File Open") 143 | .setDesc("Index new references when the file is opened") 144 | .addToggle((toggle) => { 145 | toggle.setValue(getSettings().indexOnFileOpen); 146 | toggle.onChange(async (val) => { 147 | updateSettings({ indexOnFileOpen: val }); 148 | await this.plugin.saveSettings(); 149 | }); 150 | }); 151 | 152 | new Setting(containerEl) 153 | .setName("File Edited") 154 | .setDesc("Index new references when the file is edited") 155 | .addToggle((toggle) => { 156 | toggle.setValue(getSettings().indexOnFileChange); 157 | toggle.onChange(async (val) => { 158 | updateSettings({ indexOnFileChange: val }); 159 | await this.plugin.saveSettings(); 160 | }); 161 | }); 162 | 163 | new Setting(containerEl) 164 | .setName("Layout Changed") 165 | .setDesc("Index new references when the layout is changed") 166 | .addToggle((toggle) => { 167 | toggle.setValue(getSettings().indexOnLayoutChange); 168 | toggle.onChange(async (val) => { 169 | updateSettings({ indexOnLayoutChange: val }); 170 | await this.plugin.saveSettings(); 171 | }); 172 | }); 173 | 174 | new Setting(containerEl) 175 | .setName("Type of Reference Table") 176 | .setDesc( 177 | "Choose what type of table you'd like references displayed as." 178 | ) 179 | .addDropdown((dropdown) => { 180 | const { tableType } = getSettings(); 181 | dropdown.addOption("search", "Search Results Table"); 182 | dropdown.addOption("basic", "Basic Table"); 183 | dropdown.setValue(tableType); 184 | dropdown.onChange(async (val) => { 185 | updateSettings({ tableType: val }); 186 | await this.plugin.saveSettings(); 187 | }); 188 | }); 189 | } 190 | } -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ListItemCache, 3 | Pos, 4 | SectionCache, 5 | TFile, 6 | } from "obsidian"; 7 | 8 | 9 | declare module "obsidian" { 10 | interface App { 11 | viewRegistry: { 12 | getViewCreatorByType: ( 13 | arg0: string 14 | ) => (arg0: WorkspaceLeaf) => View 15 | } 16 | internalPlugins: { 17 | getPluginById: (arg0: string) => { enabled: boolean } 18 | } 19 | } 20 | interface FileManager { 21 | getAllLinkResolutions: () => Link[] 22 | } 23 | interface Workspace { 24 | getActiveLeafOfViewType: (arg0: unknown) => WorkspaceLeaf 25 | } 26 | 27 | interface WorkspaceLeaf { 28 | tabHeaderEl: HTMLElement 29 | previewMode: { 30 | renderer: { 31 | onRendered: (arg0: () => void) => void 32 | } 33 | } 34 | } 35 | interface View { 36 | searchQuery: string 37 | currentMode: { 38 | type: string 39 | } 40 | file: TFile 41 | previewMode: { 42 | renderer: { 43 | onRendered: (arg0: unknown) => void 44 | sections: { 45 | lineStart: number 46 | lineEnd: number 47 | el: HTMLElement 48 | }[] 49 | } 50 | } 51 | } 52 | interface MetadataCache { 53 | metadataCache: { 54 | [x: string]: CachedMetadata 55 | } 56 | getLinks: () => { 57 | [key: string]: { 58 | link: string 59 | position: Pos 60 | } 61 | } 62 | } 63 | } 64 | 65 | export interface Link { 66 | reference: { 67 | link: string 68 | displayText: string 69 | position: Pos 70 | } 71 | resolvedFile: TFile 72 | resolvedPaths: string[] 73 | sourceFile: TFile 74 | } 75 | 76 | 77 | export interface TransformedCachedItem { 78 | key: string 79 | pos: Pos 80 | page: string 81 | type: string 82 | references: Link[] 83 | original?: string 84 | } 85 | 86 | export interface TransformedCache { 87 | blocks?: TransformedCachedItem[] 88 | links?: TransformedCachedItem[] 89 | headings?: TransformedCachedItem[] 90 | embeds?: TransformedCachedItem[] 91 | sections?: SectionCache[] 92 | } 93 | 94 | 95 | export interface ListItem extends ListItemCache { 96 | pos: number 97 | key: string 98 | } 99 | 100 | export interface Section { 101 | id?: string 102 | items?: ListItem[] 103 | position: Pos 104 | pos?: number 105 | type: string 106 | } 107 | -------------------------------------------------------------------------------- /src/view.ts: -------------------------------------------------------------------------------- 1 | import { App, Notice, WorkspaceLeaf } from "obsidian"; 2 | import { TransformedCachedItem, Link } from "./types"; 3 | import { getSettings } from "./settings"; 4 | 5 | export function createButtonElements( 6 | app: App, 7 | buttons: { 8 | block?: TransformedCachedItem; 9 | val?: HTMLElement; 10 | }[] 11 | ): void { 12 | buttons.forEach(({ block, val }) => { 13 | const count = block && block.references ? block.references.length : 0; 14 | const existingButton = val.querySelector("[data-count=count]"); 15 | const countEl = createCounter(block, count); 16 | if (val) { 17 | const { tableType } = getSettings(); 18 | 19 | if (tableType === "basic") { 20 | countEl.on("click", "button", () => { 21 | createRefTableElement(app, block, val); 22 | }); 23 | } 24 | if (tableType === "search") { 25 | countEl.on("click", "button", () => { 26 | createSearchElement(app, block, val); 27 | }); 28 | } 29 | if (existingButton) { 30 | existingButton.remove(); 31 | } 32 | count > 0 && val.prepend(countEl); 33 | } 34 | }); 35 | } 36 | 37 | export function createCounter( 38 | block: TransformedCachedItem, 39 | count: number 40 | ): HTMLElement { 41 | const countEl = createEl("button", { cls: "block-ref-count" }); 42 | countEl.setAttribute("data-block-ref-id", block.key); 43 | countEl.setAttribute("data-count", "count"); 44 | if (block.type === "link" || block.type === "list") { 45 | countEl.addClass("child-ref"); 46 | } else { 47 | countEl.addClass("parent-ref"); 48 | } 49 | countEl.innerText = count.toString(); 50 | return countEl; 51 | } 52 | 53 | export function createRefTableElement( 54 | app: App, 55 | block: TransformedCachedItem, 56 | val: HTMLElement 57 | ): void { 58 | const refs = block.references ? block.references : undefined; 59 | const refTable: HTMLElement = createTable(app, refs); 60 | if (!val.children.namedItem("ref-table")) { 61 | block.type === "block" && val.appendChild(refTable); 62 | block.type === "header" && val.appendChild(refTable); 63 | block.type === "link" && val.append(refTable); 64 | block.type.includes("list") && 65 | val.insertBefore(refTable, val.children[2]); 66 | } else { 67 | if (val.children.namedItem("ref-table")) { 68 | val.removeChild(refTable); 69 | } 70 | } 71 | } 72 | 73 | function buildSearchQuery(block: TransformedCachedItem): string { 74 | let page; 75 | let firstReference; 76 | let secondReference; 77 | 78 | if (block.type === "link" || block.type === "link-list") { 79 | if (block.key.includes("/")) { 80 | const keyArr = block.key.split("/"); 81 | block.key = keyArr[keyArr.length - 1]; 82 | } 83 | page = block.key; 84 | if (block.key.includes("#") && !block.key.includes("#^")) { 85 | page = block.key.split("#")[0]; 86 | if (block.original) { 87 | firstReference = `/^#{1,6} ${regexEscape(block.original)}$/`; 88 | } else { 89 | firstReference = `/^#{1,6} ${regexEscape( 90 | block.key.split("#")[1] 91 | )}/`; 92 | } 93 | secondReference = `/#${block.key.split("#")[1]}]]/`; 94 | } 95 | if (block.key.includes("#^")) { 96 | page = block.key.split("#^")[0]; 97 | firstReference = `"^${block.key.split("#^")[1]}"`; 98 | if (block.key.includes("|")) { 99 | firstReference = `${firstReference.split("|")[0]}"`; 100 | } 101 | secondReference = `"#^${block.key.split("#^")[1]}"`; 102 | } 103 | if (!firstReference) { 104 | firstReference = ""; 105 | secondReference = `"[[${block.key}]]"`; 106 | } 107 | if (block.key.includes("|")) { 108 | secondReference = 109 | secondReference + ` OR "${block.key.split("|")[0]}]]"`; 110 | } else { 111 | secondReference = secondReference + ` OR "[[${block.key}|"`; 112 | } 113 | } 114 | if (block.type === "header") { 115 | page = block.page; 116 | firstReference = `/^#{1,6} ${regexEscape(block.original)}$/`; 117 | secondReference = `/#${block.key}]]/`; 118 | } 119 | if (block.type === "block" || block.type === "block-list") { 120 | page = block.page; 121 | firstReference = `"^${block.key}"`; 122 | secondReference = `"${block.page}#^${block.key}"`; 123 | } 124 | return `(file:("${page}.md") ${firstReference}) OR (${secondReference}) `; 125 | } 126 | 127 | export async function createSearchElement( 128 | app: App, 129 | block: TransformedCachedItem, 130 | val: HTMLElement 131 | ): Promise { 132 | const normalizedKey = normalize(block.key); 133 | const searchEnabled = 134 | app.internalPlugins.getPluginById("global-search").enabled; 135 | if (!searchEnabled) { 136 | new Notice("you need to enable the core search plugin"); 137 | } else { 138 | const tempLeaf = app.workspace.getRightLeaf(false); 139 | //Hide the leaf/pane so it doesn't show up in the right sidebar 140 | tempLeaf.tabHeaderEl.hide(); 141 | await tempLeaf.setViewState({ 142 | type: "search-ref", 143 | state: { 144 | query: buildSearchQuery(block), 145 | }, 146 | }); 147 | const search = app.workspace.getLeavesOfType("search-ref"); 148 | const searchElement = createSearch(search, block); 149 | const searchHeight = 300; 150 | searchElement.setAttribute("style", "height: " + searchHeight + "px;"); 151 | 152 | if (!val.children.namedItem("search-ref")) { 153 | search[search.length - 1].view.searchQuery; 154 | // depending on the type of block the search view needs to be inserted into the DOM at different points 155 | block.type === "block" && val.appendChild(searchElement); 156 | block.type === "header" && val.appendChild(searchElement); 157 | block.type === "link" && val.append(searchElement); 158 | block.type.includes("list") && 159 | val.insertBefore(searchElement, val.children[2]); 160 | } else { 161 | if (val.children.namedItem("search-ref")) { 162 | app.workspace.getLeavesOfType("search-ref").forEach((leaf) => { 163 | const container = leaf.view.containerEl; 164 | const dataKey = `[data-block-ref-id='${normalizedKey}']`; 165 | const key = container.parentElement.querySelector(dataKey); 166 | if (key) { 167 | leaf.detach(); 168 | } 169 | }); 170 | } 171 | } 172 | } 173 | } 174 | 175 | 176 | function createSearch( 177 | search: WorkspaceLeaf[], 178 | block: TransformedCachedItem 179 | ) { 180 | const searchElement = search[search.length - 1].view.containerEl; 181 | const normalizedKey = normalize(block.key); 182 | searchElement.setAttribute("data-block-ref-id", normalizedKey); 183 | searchElement.setAttribute("id", "search-ref"); 184 | return searchElement; 185 | } 186 | 187 | function createTable(app: App, refs: Link[]): HTMLElement { 188 | const refTable = createEl("table", { cls: "ref-table" }); 189 | refTable.setAttribute("id", "ref-table"); 190 | const noteHeaderRow = createEl("tr").createEl("th", { text: "Note" }); 191 | 192 | const lineHeaderRow = createEl("tr").createEl("th", { 193 | text: "Reference", 194 | cls: "reference", 195 | }); 196 | 197 | refTable.appendChild(noteHeaderRow); 198 | refTable.appendChild(lineHeaderRow); 199 | refs && 200 | refs.forEach(async (ref) => { 201 | const lineContent = await app.vault 202 | .cachedRead(ref.sourceFile) 203 | .then( 204 | (content) => 205 | content.split("\n")[ref.reference.position.start.line] 206 | ); 207 | const row = createEl("tr"); 208 | const noteCell = createEl("td"); 209 | const lineCell = createEl("td"); 210 | noteCell.createEl("a", { 211 | cls: "internal-link", 212 | href: ref.sourceFile.path, 213 | text: ref.sourceFile.basename, 214 | }); 215 | 216 | lineCell.createEl("span", { text: lineContent }); 217 | row.appendChild(noteCell); 218 | row.appendChild(lineCell); 219 | refTable.appendChild(row); 220 | }); 221 | return refTable; 222 | } 223 | 224 | function regexEscape(string: string) { 225 | return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); 226 | } 227 | 228 | const normalize = (str: string) => { 229 | return str.replace(/\s+|'/g, "").toLowerCase(); 230 | }; 231 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .block-ref-count { 2 | float: right; 3 | color: var(--text-muted); 4 | size: .5rem; 5 | padding: 5px; 6 | width: 25px; 7 | height: 20px; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | position: relative; 12 | left: 20px; 13 | } 14 | 15 | .block-ref-count:hover { 16 | color: var(--text-normal); 17 | border: 1px var(--text-accent); 18 | } 19 | 20 | .highlight-animation { 21 | transition: background-color 0.5s ease; 22 | } 23 | 24 | [data-block-ref-id] .search-result-file-matched-text { 25 | background-color: transparent; 26 | color: var(--text-muted); 27 | } 28 | 29 | [data-block-ref-id] .search-input-container, [data-block-ref-id] [aria-label="Match case"], [data-block-ref-id] [aria-label="Explain search term"] { 30 | display: none; 31 | } 32 | 33 | [data-block-ref-id] .nav-buttons-container .search-input-clear-button { 34 | background: transparent; 35 | position: relative; 36 | padding: 5px 8px 0 8px; 37 | margin: 0 3px 10px 3px; 38 | top: unset; 39 | right: unset; 40 | bottom: 5px; 41 | } 42 | 43 | 44 | .is-mobile .block-ref-count { 45 | width: unset 46 | } 47 | 48 | .table-close { 49 | color: var(--text-muted); 50 | } 51 | -------------------------------------------------------------------------------- /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 | "es6", 15 | "scripthost", 16 | "es2019" ] 17 | }, 18 | "include": [ 19 | "**/*.ts" 20 | , "src/IndexWorker.ts" ] 21 | } 22 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "0.0.2": "0.11.0" 3 | } 4 | --------------------------------------------------------------------------------