├── constants.ts ├── .npmrc ├── .eslintignore ├── versions.json ├── README.md ├── .editorconfig ├── manifest.json ├── .gitignore ├── tsconfig.json ├── src ├── getHighlights.ts └── view.ts ├── version-bump.mjs ├── .eslintrc ├── styles.css ├── package.json ├── esbuild.config.mjs └── main.ts /constants.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | main.js 4 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.15.0" 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | (Very WIP) 2 | 3 | A sidebar view that shows a list of highlights for the active file. 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "highlights-view", 3 | "name": "Highlights View", 4 | "version": "1.0.0", 5 | "minAppVersion": "0.15.0", 6 | "description": "Show highlights in a sidebar view.", 7 | "author": "@kepano", 8 | "authorUrl": "https://obsidian.md", 9 | "fundingUrl": "https://obsidian.md/pricing", 10 | "isDesktopOnly": false 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /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 | "isolatedModules": true, 13 | "strictNullChecks": true, 14 | "lib": [ 15 | "DOM", 16 | "ES5", 17 | "ES6", 18 | "ES7" 19 | ] 20 | }, 21 | "include": [ 22 | "**/*.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/getHighlights.ts: -------------------------------------------------------------------------------- 1 | export default class GetHighlights { 2 | process(data: string): { id: string, text: string }[] { 3 | let re: RegExp = /(==|\)([\s\S]*?)(==|\<\/mark\>)/g; 4 | let matches = data.match(re); 5 | let highlights: { id: string, text: string }[] = []; 6 | 7 | if (matches) { 8 | matches.forEach((match) => { 9 | let cleanText = match 10 | .replace(/==/g, "") 11 | .replace(/\/g, "") 12 | .replace(/\<\/mark\>/g, "") 13 | .replace(/\*\*/g, ""); 14 | highlights.push({ id: cleanText, text: cleanText }); 15 | }); 16 | } 17 | 18 | return highlights; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .highlights-list .highlight { 2 | cursor: var(--cursor); 3 | position: relative; 4 | padding: var(--size-4-2) var(--size-4-3) var(--size-4-2) var(--size-4-3); 5 | white-space: pre-wrap; 6 | width: 100%; 7 | font-size: var(--font-ui-small); 8 | line-height: var(--line-height-tight); 9 | background-color: var(--search-result-background); 10 | border-left: 5px solid var(--text-highlight-bg); 11 | border-radius: var(--radius-s); 12 | overflow: hidden; 13 | margin: 0 0 var(--size-4-2); 14 | color: var(--text-normal); 15 | box-shadow: 0 0 0 1px var(--background-modifier-border); 16 | } 17 | 18 | .highlights-list .highlight:hover { 19 | color: var(--text-normal); 20 | background-color: var(--text-selection); 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-sample-plugin", 3 | "version": "1.0.0", 4 | "description": "This is a sample plugin for Obsidian (https://obsidian.md)", 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 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/node": "^16.11.6", 16 | "@typescript-eslint/eslint-plugin": "5.29.0", 17 | "@typescript-eslint/parser": "5.29.0", 18 | "builtin-modules": "3.3.0", 19 | "esbuild": "0.17.3", 20 | "obsidian": "latest", 21 | "tslib": "2.4.0", 22 | "typescript": "4.7.4" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | 5 | const banner = 6 | `/* 7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 8 | if you want to view the source, please visit the github repository of this plugin 9 | */ 10 | `; 11 | 12 | const prod = (process.argv[2] === "production"); 13 | 14 | const context = await esbuild.context({ 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: ["main.ts"], 19 | bundle: true, 20 | external: [ 21 | "obsidian", 22 | "electron", 23 | "@codemirror/autocomplete", 24 | "@codemirror/collab", 25 | "@codemirror/commands", 26 | "@codemirror/language", 27 | "@codemirror/lint", 28 | "@codemirror/search", 29 | "@codemirror/state", 30 | "@codemirror/view", 31 | "@lezer/common", 32 | "@lezer/highlight", 33 | "@lezer/lr", 34 | ...builtins], 35 | format: "cjs", 36 | target: "es2018", 37 | logLevel: "info", 38 | sourcemap: prod ? false : "inline", 39 | treeShaking: true, 40 | outfile: "main.js", 41 | }); 42 | 43 | if (prod) { 44 | await context.rebuild(); 45 | process.exit(0); 46 | } else { 47 | await context.watch(); 48 | } -------------------------------------------------------------------------------- /src/view.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ItemView, 3 | MarkdownView, 4 | setIcon, 5 | WorkspaceLeaf 6 | } from 'obsidian'; 7 | 8 | import GetHighlights from 'src/getHighlights'; 9 | import HighlightsView from './main'; 10 | 11 | export const viewType = 'HighlightsView'; 12 | 13 | export class HighlightsItemView extends ItemView { 14 | plugin: HighlightsView; 15 | activeMarkdownLeaf: MarkdownView; 16 | 17 | constructor(leaf: WorkspaceLeaf, plugin: HighlightsView) { 18 | super(leaf); 19 | this.plugin = plugin; 20 | 21 | this.contentEl.addClass('highlights-list'); 22 | this.setNoContentMessage(); 23 | } 24 | 25 | async setViewContent() { 26 | const activeLeaf = this.app.workspace.activeLeaf; 27 | if (activeLeaf && activeLeaf.view instanceof MarkdownView) { 28 | const markdownContent = await activeLeaf.view.data; 29 | const processor = new GetHighlights(); 30 | const highlights = processor.process(markdownContent); 31 | 32 | this.contentEl.empty(); 33 | 34 | highlights.forEach(({ id, text }) => { 35 | const div = this.contentEl.createDiv( 36 | { cls: 'highlight', text: text } 37 | ); 38 | div.addEventListener('click', () => { 39 | this.scrollToHighlight(id); 40 | }); 41 | }); 42 | } else { 43 | this.setNoContentMessage(); 44 | } 45 | } 46 | 47 | scrollToHighlight(highlightId: string) { 48 | const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); 49 | if (!activeView) return; 50 | 51 | // todo: click to scroll to document position 52 | } 53 | 54 | setNoContentMessage() { 55 | this.setMessage('No highlights found.'); 56 | } 57 | 58 | setMessage(message: string) { 59 | this.contentEl.empty(); 60 | this.contentEl.createDiv({ 61 | cls: 'pane-empty', 62 | text: message, 63 | }); 64 | } 65 | 66 | getViewType(): string { 67 | return viewType; 68 | } 69 | 70 | getDisplayText(): string { 71 | return "Highlights"; 72 | } 73 | 74 | getIcon(): string { 75 | return 'highlighter'; 76 | } 77 | 78 | 79 | } 80 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | debounce, 4 | Events, 5 | MarkdownView, 6 | Menu, 7 | Plugin, 8 | setIcon, 9 | View, 10 | WorkspaceLeaf 11 | } from 'obsidian'; 12 | 13 | import { HighlightsItemView, viewType } from 'src/view'; 14 | 15 | interface HighlightsViewSettings { 16 | } 17 | 18 | const DEFAULT_SETTINGS: HighlightsViewSettings = { 19 | } 20 | 21 | export default class HighlightsView extends Plugin { 22 | settings: HighlightsViewSettings; 23 | 24 | async onload() { 25 | const { app } = this; 26 | this.initLeaf(); 27 | 28 | await this.loadSettings(); 29 | 30 | this.registerView( 31 | viewType, 32 | (leaf: WorkspaceLeaf) => new HighlightsItemView(leaf, this) 33 | ); 34 | 35 | this.addCommand({ 36 | id: 'open-highlights-view', 37 | name: 'Open highlights view', 38 | callback: () => { 39 | this.initLeaf(); 40 | } 41 | }); 42 | 43 | this.registerEvent( 44 | app.metadataCache.on( 45 | 'changed', 46 | debounce( 47 | async (file) => { 48 | const activeView = app.workspace.getActiveViewOfType(MarkdownView); 49 | if (activeView && file === activeView.file) { 50 | this.processHighlights(); 51 | } 52 | }, 53 | 100, 54 | true 55 | ) 56 | ) 57 | ); 58 | 59 | this.registerEvent( 60 | app.workspace.on( 61 | 'active-leaf-change', 62 | debounce( 63 | async (leaf) => { 64 | app.workspace.iterateRootLeaves((rootLeaf) => { 65 | if (rootLeaf === leaf) { 66 | if (leaf.view instanceof MarkdownView) { 67 | this.processHighlights(); 68 | } else { 69 | this.view?.setNoContentMessage(); 70 | } 71 | } 72 | }); 73 | }, 74 | 100, 75 | true 76 | ) 77 | ) 78 | ); 79 | 80 | (async () => { 81 | this.processHighlights(); 82 | })(); 83 | } 84 | 85 | onunload() { 86 | this.app.workspace 87 | .getLeavesOfType(viewType) 88 | .forEach((leaf) => leaf.detach()); 89 | } 90 | 91 | get view() { 92 | const leaves = this.app.workspace.getLeavesOfType(viewType); 93 | if (!leaves?.length) return null; 94 | return leaves[0].view as HighlightsView; 95 | } 96 | 97 | async initLeaf() { 98 | if (this.view) return this.revealLeaf(); 99 | 100 | await this.app.workspace.getRightLeaf(false).setViewState({ 101 | type: viewType, 102 | }); 103 | 104 | this.revealLeaf(); 105 | 106 | const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); 107 | if (activeView) { 108 | this.processHighlights(); 109 | } 110 | } 111 | 112 | revealLeaf() { 113 | const leaves = this.app.workspace.getLeavesOfType(viewType); 114 | if (!leaves?.length) return; 115 | this.app.workspace.revealLeaf(leaves[0]); 116 | } 117 | 118 | async loadSettings() { 119 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); 120 | } 121 | 122 | async saveSettings() { 123 | await this.saveData(this.settings); 124 | } 125 | 126 | processHighlights = async () => { 127 | const { settings, view } = this; 128 | 129 | const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); 130 | 131 | if (activeView) { 132 | view?.setViewContent(); 133 | } else { 134 | view?.setNoContentMessage(); 135 | } 136 | }; 137 | 138 | } 139 | --------------------------------------------------------------------------------