[
  {
    "path": ".editorconfig",
    "content": "# top-most EditorConfig file\r\nroot = true\r\n\r\n[*]\r\ncharset = utf-8\r\nend_of_line = lf\r\ninsert_final_newline = true\r\nindent_style = tab\r\nindent_size = 4\r\ntab_width = 4\r\n"
  },
  {
    "path": ".eslintignore",
    "content": "node_modules/\n\nmain.js\n"
  },
  {
    "path": ".eslintrc",
    "content": "{\n    \"root\": true,\n    \"parser\": \"@typescript-eslint/parser\",\n    \"env\": { \"node\": true },\n    \"plugins\": [\n      \"@typescript-eslint\"\n    ],\n    \"extends\": [\n      \"eslint:recommended\",\n      \"plugin:@typescript-eslint/eslint-recommended\",\n      \"plugin:@typescript-eslint/recommended\"\n    ], \n    \"parserOptions\": {\n        \"sourceType\": \"module\"\n    },\n    \"rules\": {\n      \"no-unused-vars\": \"off\",\n      \"@typescript-eslint/no-unused-vars\": [\"error\", { \"args\": \"none\" }],\n      \"@typescript-eslint/ban-ts-comment\": \"off\",\n      \"no-prototype-builtins\": \"off\",\n      \"@typescript-eslint/no-empty-function\": \"off\"\n    } \n  }"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release Obsidian plugin\n\non:\n  push:\n    tags:\n      - \"*\"\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Use Node.js\n        uses: actions/setup-node@v3\n        with:\n          node-version: \"18.x\"\n\n      - name: Build plugin\n        run: |\n          npm install\n          npm run build\n\n      - name: Create release\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          tag=\"${GITHUB_REF#refs/tags/}\"\n\n          gh release create \"$tag\" \\\n            --title=\"$tag\" \\\n            --draft \\\n            main.js manifest.json styles.css\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test\n\non:\n  pull_request:\n    branches:\n      - master\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v5\n      - name: Use Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: '20.x'\n      - run: npm ci\n      - run: npm run build --if-present\n      - run: npm test\n"
  },
  {
    "path": ".gitignore",
    "content": "# vscode\r\n.vscode \r\n\r\n# Intellij\r\n*.iml\r\n.idea\r\n\r\n# npm\r\nnode_modules\r\n\r\n# Don't include the compiled main.js file in the repo.\r\n# They should be uploaded to GitHub releases instead.\r\nmain.js\r\n\r\n# Exclude sourcemaps\r\n*.map\r\n\r\n# obsidian\r\ndata.json\r\n\r\n# Exclude macOS Finder (System Explorer) View States\r\n.DS_Store\r\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "npm test\nnpx lint-staged\n"
  },
  {
    "path": ".npmrc",
    "content": "tag-version-prefix=\"\""
  },
  {
    "path": ".prettierignore",
    "content": "# Ignore docs\n**/*.md\n\n# JS files are generated, no need to format\n*.js\n*.mjs\n\n# Ignore github files\n.github/\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 dominiclet\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Obsidian Note Definitions\n\nA personal dictionary that can be easily looked-up from within your notes.\n\n![dropdown](./img/def-dropdown.png)\n\n## Basic usage\n\n1. Create a folder, right-click on the folder in your file explorer, and select `Set definition folder`. This registers the folder as your definition folder.\n2. Within the folder, create definition files (with any name of your choice).\n3. Add a definition using the `Add definition` command. This will display a pop-up modal, where you can input your definition.\n4. Once a definition is added, the word/phrase should be underlined in your notes. You may preview the definition of the word by hovering over the underlined word/phrase with the mouse, or triggering the `Preview definition` command when your cursor is on the word/phrase.\n\n### Editor menu\n\nOptions available:\n- Go to definition (jump to definition of word/phrase)\n- Add definition (the text that you want to define must be highlighted for this to be available)\n- Edit definition (right-click on an underlined definition)\n\n### Commands\n\nYou may want to assign hotkeys to the commands available for easy access:\n- Preview definition (show definition popover)\n- Go to definition (jump to definition of word/phrase)\n- Add definition\n- Add definition context (see [Definition context](#definition-context))\n- Register consolidated definition file\n- Register atomic definition file\n- Refresh definitions\n\n## How it works\n\n**Note Definitions** does not maintain any hidden metadata files for your definitions. \nAll definitions are placed in your vault and form part of your notes.\nYou will notice that added definitions will create entries within your selected definition file. \nYou may edit these entries freely to add/edit your definitions, but if you do so, make sure to adhere strictly to the definition rules below.\n**It is recommended that you read through the definition rules first before manually editing the definition files.**\n\n### Definition rules\n\nCurrently, there are two types of definition files: `consolidated` and `atomic`.\nThe type of definition file is specified in the `def-type` frontmatter (or property) of a file.\nFor all definition files you create, the `def-type` frontmatter should be set to either 'consolidated' or 'atomic'.\nFor compatibility reasons, a file is treated to be `consolidated` if the `def-type` frontmatter is not specified (but this is not guaranteed to remain the same in subsequent releases, so always specify the frontmatter when creating a new definition file). \nFor convenience, use the commands provided to add the `def-type` frontmatter.\n\n#### Consolidated definition file\n\nA `consolidated` file type refers to a file that can contain many definitions.\nRegister a definition file by specifying the `def-type: consolidated` frontmatter, or using the `Register consolidated definition file` command when the file is active.\n\nA `consolidated` definition file is parsed according to the following rules:\n\n1. A definition block consists of a **phrase (1 or more words), an alias (optional) and a definition**. They must be provided **strictly** in that order.\n2. A phrase is denoted with a line in the following format `# <PHRASE>`. This is rendered as a markdown header in Obsidian.\n3. An **optional** comma-separated line of alias(es) is expected after a phrase. This must be a line surrounded by asterisks, eg. `*alias*`. *This is rendered as italics in Obsidian*.\n4. A line that occurs after a registered **phrase** and is not an alias is deemed to be a definition. Definitions can be multi-line. All subsequent lines are definitions until the definition block divider is encountered. You may write markdown here, which will be formatted similar to Obsidian's markdown formatting.\n5. A line with nothing but three hyphens `---` is used as a divider to separate definition blocks. This is rendered as a delimiting line in Obsidian. (This divider can be configured in the settings to recognise three underscores `___` as well)\n\nExample definition file:\n\n> # Word1\n> \n> *alias of word1*\n> \n> Definition of word1.\n> This definition can span several lines.\n> It will end when the divider is reached.\n> \n> ---\n> \n> # Word2\n>\n> Notice that there is no alias here as it is optional.\n> The last word in the file does not need to have a divider, although it is still valid to have one.\n> \n> ---\n> \n> # Phrase with multiple words\n> \n> You can also define a phrase containing multiple words. \n>\n> ---\n>\n> # Markdown support\n> \n> Markdown is supported so you can do things like including *italics* or **bold** words.\n\nFor a more formal definition of the grammar of the consolidated definition file, you may refer to [this document](docs/grammar.md). \n\n#### Atomic definition file\n\nAn `atomic` definition file refers to a file that contains only one definition.\nRegister an atomic definition file by specifying the `def-type: atomic` frontmatter, or using the `Register atomic definition file` command when the file is active.\n\nAn `atomic` definition file is parsed according to the following rules:\n1. The name of the file is the word/phrase defined\n2. Aliases are specified in the `aliases` frontmatter as a list. In source, it should look something like this:\n```\n---\naliases:\n  - alias1\n  - alias2\n---\n```\n3. The contents of the file (excluding the frontmatter) form the definition\n\n## Definition context\n> _TLDR:_ \"Context\" is synonymous with a definition file. By specifying a context, you specify that you want to use specific definition file(s) to source your definitions for the current note.\n\nDefinition context refers to the repository of definitions that are available for the currently active note.\nBy default, all notes have no context (you can think of this as being globally-scoped).\nThis means that your newly-created notes will always have access to the combination of all definitions defined in your definition files.\n\nThis behaviour can be overridden by specifying the \"context\" of your note.\nEach definition file that you have is taken to be a separate context (hence your definitions should be structured accordingly).\nOnce context(s) are declared for a note, it will only retrieve definitions from the specified contexts.\nYou can think of this as having a local scope for the note.\nThe note now sees only a limited subset of all your definitions.\n\n### Usage\n\nTo easily add context to your note:\n1. Use the `Add definition context` command\n2. Search and select your desired context\n\nYou can do this multiple times to add multiple contexts.\n\n### How it works\n\n`Add definition context` adds to the _properties_ of your note.\nSpecifically, it adds to the `def-context` property, which is a `List` type containing a list of file paths corresponding to the selected definition files.\nIn source, it will look something like this:\n```\n---\ndef-context:\n\t- definitions/def1.md\n\t- definitions/def2.md\n---\n```\n\nYou can edit your properties directly, although for convenience, it is recommended to use the `Add definition context` command to add contexts as it is easy to get file paths wrong.\n\n### Removing contexts\n\nTo remove contexts, simply remove the file path from the `def-context` property.\nOr if you want to remove all contexts, you can delete the `def-context` property altogether.\n\n## Refreshing definitions\n\nWhenever you find that the plugin is not detecting certain definitions or definition files, run the `Refresh definitions` command to manually get the plugin to read your definition files.\n\n## Feedback\n\nI welcome any feedback on how to improve this tool.\nDo let me know by opening a Github issue if you find any bugs, or have any ideas for features or improvements.\n\n## Contributing\n\nIf you're a programmer and would like to see certain features implemented, I welcome and would be grateful for contributions. If you are interested, please do let me know in the issue thread.\n"
  },
  {
    "path": "__mocks__/internals.ts",
    "content": "export class DefManager {\n\tloadUpdatedFiles() {}\n}\n"
  },
  {
    "path": "__mocks__/obsidian.ts",
    "content": "export class App {\n\tvault: Vault;\n\tmetadataCache: MetadataCache;\n\n\tconstructor() {\n\t\tthis.vault = new Vault();\n\t\tthis.metadataCache = new MetadataCache();\n\t}\n}\n\nexport class TFile {\n\tbasename: string;\n\textension: string;\n\t\n\t// Ignore other properties\n}\n\nexport class PluginSettingTab {}\n\nexport class Vault {\n\tmodify(file: TFile, data: string) {}\n\tread(file: TFile): Promise<string> {\n\t\treturn Promise.resolve(\"\");\n\t}\n}\n\nexport class MetadataCache {\n\tgetFileCache(file: TFile) {\n\t\treturn null;\n\t}\n}\n\nexport class Notice {}\n"
  },
  {
    "path": "babel.config.js",
    "content": "module.exports = {presets: ['@babel/preset-env', \"@babel/preset-typescript\"]}\n"
  },
  {
    "path": "docs/grammar.md",
    "content": "# Definition File Grammar\n\nThis file documents the formal grammar defined for the definition files. It should give you some insight into how your definition files are parsed, if the README documentation is insufficient.\nIf you notice that the behaviour of the parser departs from the documented grammar here, do let me know by raising an issue.\n\nThe following is written in extended Backus-Naur form.\n\n## Consolidated definition file grammar\n\n```text\ndoc = { def-block, [ delimiter, { \"\\n\" }] }, eof;\ndef-block = header, \"\\n\", [ alias, { char }, \"\\n\" ], def;\nheader = \"#\", \" \", { char };\nalias = { \"\\n\" }, \"*\", [{ { char }, \",\" }], { char }, \"*\";\ndef = { char };\nchar = any char;\ndelimiter = \"\\n\", { \" \" }, \"-\", \"-\", \"-\", { \" \" }, \"\\n\";\n```\n\nThe only terminal is an end-of-file after all def-blocks. If a delimeter is given, then another def-block is expected.\nSo a valid file should not provide a trailing delimiter at the end of the file.\n"
  },
  {
    "path": "esbuild.config.mjs",
    "content": "import esbuild from \"esbuild\";\nimport process from \"process\";\nimport builtins from \"builtin-modules\";\n\nconst banner =\n`/*\nTHIS IS A GENERATED/BUNDLED FILE BY ESBUILD\nif you want to view the source, please visit the github repository of this plugin\n*/\n`;\n\nconst prod = (process.argv[2] === \"production\");\n\nconst context = await esbuild.context({\n\tbanner: {\n\t\tjs: banner,\n\t},\n\tentryPoints: [\"src/main.ts\"],\n\tbundle: true,\n\texternal: [\n\t\t\"obsidian\",\n\t\t\"electron\",\n\t\t\"@codemirror/autocomplete\",\n\t\t\"@codemirror/collab\",\n\t\t\"@codemirror/commands\",\n\t\t\"@codemirror/language\",\n\t\t\"@codemirror/lint\",\n\t\t\"@codemirror/search\",\n\t\t\"@codemirror/state\",\n\t\t\"@codemirror/view\",\n\t\t\"@lezer/common\",\n\t\t\"@lezer/highlight\",\n\t\t\"@lezer/lr\",\n\t\t...builtins],\n\tformat: \"cjs\",\n\ttarget: \"es2018\",\n\tlogLevel: \"info\",\n\tsourcemap: prod ? false : \"inline\",\n\ttreeShaking: true,\n\toutfile: \"main.js\",\n});\n\nif (prod) {\n\tawait context.rebuild();\n\tprocess.exit(0);\n} else {\n\tawait context.watch();\n}\n"
  },
  {
    "path": "jest.config.js",
    "content": "module.exports = {\n    preset: \"ts-jest\",\n    testEnvironment: \"node\",\n\ttransform: {\n\t\t'^.+\\\\.ts$': 'ts-jest',\n\t},\n\tmoduleDirectories: [\"node_modules\", \"<rootDir>\"],\n\tmoduleFileExtensions: [\"ts\", \"js\"],\n\troots: [\"<rootDir>\"],\n\tmodulePaths: [\"<rootDir>\"],\n};\n"
  },
  {
    "path": "manifest.json",
    "content": "{\n\t\"id\": \"note-definitions\",\n\t\"name\": \"Note Definitions\",\n\t\"version\": \"0.29.1\",\n\t\"minAppVersion\": \"1.5.12\",\n\t\"description\": \"Personal dictionary for your notes\",\n\t\"author\": \"Dominic Let\",\n\t\"isDesktopOnly\": false\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n\t\"name\": \"obsidian-note-definitions\",\n\t\"version\": \"0.29.1\",\n\t\"description\": \"Personal dictionary for your notes\",\n\t\"main\": \"main.js\",\n\t\"scripts\": {\n\t\t\"dev\": \"node esbuild.config.mjs\",\n\t\t\"build\": \"tsc -noEmit -skipLibCheck && node esbuild.config.mjs production\",\n\t\t\"version\": \"node version-bump.mjs && git add manifest.json versions.json\",\n\t\t\"test\": \"jest --config ./jest.config.js\",\n\t\t\"prepare\": \"husky\"\n\t},\n\t\"keywords\": [],\n\t\"author\": \"\",\n\t\"license\": \"MIT\",\n\t\"devDependencies\": {\n\t\t\"@types/jest\": \"^29.5.14\",\n\t\t\"@types/node\": \"^16.11.6\",\n\t\t\"@typescript-eslint/eslint-plugin\": \"5.29.0\",\n\t\t\"@typescript-eslint/parser\": \"5.29.0\",\n\t\t\"builtin-modules\": \"3.3.0\",\n\t\t\"esbuild\": \"0.17.3\",\n\t\t\"husky\": \"^9.1.7\",\n\t\t\"jest\": \"^29.7.0\",\n\t\t\"lint-staged\": \"^16.2.7\",\n\t\t\"obsidian\": \"latest\",\n\t\t\"prettier\": \"3.8.1\",\n\t\t\"ts-jest\": \"^29.2.5\",\n\t\t\"tslib\": \"2.4.0\",\n\t\t\"typescript\": \"4.7.4\"\n\t},\n\t\"dependencies\": {\n\t\t\"pluralize\": \"^8.0.0\"\n\t},\n\t\"lint-staged\": {\n\t\t\"*.{ts,tsx,css,scss,json}\": \"prettier --write\"\n\t}\n}\n"
  },
  {
    "path": "src/core/atomic-def-parser.ts",
    "content": "import { BaseDefParser } from \"./base-def-parser\";\nimport { App, TFile } from \"obsidian\";\nimport { Definition } from \"./model\";\nimport { DefFileType } from \"./file-type\";\n\nexport class AtomicDefParser extends BaseDefParser {\n\tapp: App;\n\tfile: TFile;\n\n\tconstructor(app: App, file: TFile) {\n\t\tsuper();\n\n\t\tthis.app = app;\n\t\tthis.file = file;\n\t}\n\n\tasync parseFile(fileContent?: string): Promise<Definition[]> {\n\t\tif (!fileContent) {\n\t\t\tfileContent = await this.app.vault.cachedRead(this.file);\n\t\t}\n\n\t\tconst fileMetadata = this.app.metadataCache.getFileCache(this.file);\n\t\tlet aliases = [];\n\t\tconst fmData = fileMetadata?.frontmatter;\n\t\tif (fmData) {\n\t\t\tconst fmAlias = fmData[\"aliases\"];\n\t\t\tif (Array.isArray(fmAlias)) {\n\t\t\t\taliases = fmAlias;\n\t\t\t}\n\t\t}\n\t\tconst fmPos = fileMetadata?.frontmatterPosition;\n\t\tif (fmPos) {\n\t\t\tfileContent = fileContent.slice(fmPos.end.offset + 1);\n\t\t}\n\n\t\tlet key = this.parseSettings.enableCaseSensitive ? this.file.basename : this.file.basename.toLowerCase();\n\t\t\n\t\taliases = aliases.concat(\n\t\t\tthis.calculatePlurals([key].concat(aliases)),\n\t\t);\n\n\t\tconst def = {\n\t\t\tkey: key,\n\t\t\tword: this.file.basename,\n\t\t\taliases: aliases,\n\t\t\tdefinition: fileContent,\n\t\t\tfile: this.file,\n\t\t\tlinkText: `${this.file.path}`,\n\t\t\tfileType: DefFileType.Atomic,\n\t\t};\n\t\treturn [def];\n\t}\n}\n"
  },
  {
    "path": "src/core/base-def-parser.ts",
    "content": "import { DefFileParseConfig, getSettings } from \"src/settings\";\n\nvar pluralize = require(\"pluralize\");\n\nexport class BaseDefParser {\n\tparseSettings: DefFileParseConfig;\n\n\tconstructor(parseSettings?: DefFileParseConfig) {\n\t\tthis.parseSettings = parseSettings\n\t\t\t? parseSettings\n\t\t\t: this.getParseSettings();\n\t}\n\n\tcalculatePlurals(aliases: string[]) {\n\t\tlet plurals: string[] = [];\n\n\t\tif (this.parseSettings.autoPlurals) {\n\t\t\taliases.forEach((alias) => {\n\t\t\t\tlet pl = pluralize(alias);\n\t\t\t\tif (pl !== alias) {\n\t\t\t\t\tplurals.push(pl);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\treturn plurals;\n\t}\n\n\tgetParseSettings(): DefFileParseConfig {\n\t\treturn getSettings().defFileParseConfig;\n\t}\n}\n"
  },
  {
    "path": "src/core/consolidated-def-parser.ts",
    "content": "import { App, TFile } from \"obsidian\";\nimport { BaseDefParser } from \"src/core/base-def-parser\";\nimport { DefFileParseConfig } from \"src/settings\";\nimport { DefFileType } from \"./file-type\";\nimport { Definition, FilePosition } from \"./model\";\n\ninterface DocAST {\n\tblocks: DefblockAST[];\n}\n\ninterface DefblockAST {\n\theader: string;\n\taliases: string[];\n\tbody: string;\n\tposition: FilePosition;\n}\n\nconst EOF = \"\";\n\nexport class ConsolidatedDefParser extends BaseDefParser {\n\tapp: App;\n\tfile: TFile;\n\tparseSettings: DefFileParseConfig;\n\n\tfileContent: string;\n\tcursor: number;\n\tcurrLine: number;\n\n\tconstructor(app: App, file: TFile, parseSettings?: DefFileParseConfig) {\n\t\tsuper(parseSettings);\n\n\t\tthis.app = app;\n\t\tthis.file = file;\n\n\t\tthis.parseSettings = parseSettings\n\t\t\t? parseSettings\n\t\t\t: this.getParseSettings();\n\n\t\tthis.fileContent = \"\";\n\t\tthis.currLine = 0;\n\t}\n\n\tasync parseFile(fileContent?: string): Promise<Definition[]> {\n\t\tif (fileContent === \"\") {\n\t\t\treturn [];\n\t\t}\n\t\tif (!fileContent) {\n\t\t\tfileContent = await this.app.vault.cachedRead(this.file);\n\t\t}\n\n\t\t// Ignore frontmatter (properties)\n\t\tconst fileMetadata = this.app.metadataCache.getFileCache(this.file);\n\t\tconst fmPos = fileMetadata?.frontmatterPosition;\n\t\tif (fmPos) {\n\t\t\tfileContent = fileContent.slice(fmPos.end.offset + 1);\n\t\t}\n\t\treturn this.directParseFile(fileContent);\n\t}\n\n\t// Parse from string, no dependency on App\n\t// For ease of testing\n\tdirectParseFile(fileContent: string): Definition[] {\n\t\tthis.fileContent = fileContent;\n\t\tthis.currLine = 0;\n\t\tthis.cursor = 0;\n\t\tconst doc = this.parseDoc();\n\t\treturn doc.blocks.map((blk) => this.defBlockToDefinition(blk));\n\t}\n\n\tprivate parseDoc(): DocAST {\n\t\tconst blocks = [];\n\t\twhile (this.cursor < this.fileContent.length) {\n\t\t\t// Ignore leading newlines (and whitespace)\n\t\t\tlet c;\n\t\t\tdo {\n\t\t\t\tc = this.consumeChar();\n\t\t\t} while (/\\s/.test(c));\n\n\t\t\t// If EOF encountered, just return\n\t\t\tif (c === EOF) {\n\t\t\t\treturn {\n\t\t\t\t\tblocks,\n\t\t\t\t};\n\t\t\t}\n\t\t\t// otherwise return character to def block\n\t\t\tthis.spitChar();\n\n\t\t\tblocks.push(this.parseDefBlock());\n\t\t}\n\t\treturn {\n\t\t\tblocks,\n\t\t};\n\t}\n\n\tprivate parseDefBlock(): DefblockAST {\n\t\tconst posStart = this.currLine;\n\t\tlet header = this.parseHeader();\n\t\tlet aliases = this.parseAliases();\n\t\tlet def = this.parseDef();\n\t\tconst posEnd = this.currLine - 1;\n\t\treturn {\n\t\t\theader,\n\t\t\taliases,\n\t\t\tbody: def,\n\t\t\tposition: {\n\t\t\t\tfrom: posStart,\n\t\t\t\tto: posEnd,\n\t\t\t},\n\t\t};\n\t}\n\n\tprivate parseHeader(): string {\n\t\tconst h = this.consumeChar();\n\n\t\tif (h != \"#\") {\n\t\t\tthrow new Error(\n\t\t\t\t`Parse Header for ${this.file.path} (at line ${this.currLine}): Unexpected character '${h}', expected '#'`,\n\t\t\t);\n\t\t}\n\t\tlet s = this.consumeChar();\n\t\tif (s != \" \") {\n\t\t\tthrow new Error(\n\t\t\t\t`Parse Header for ${this.file.path} (at line ${this.currLine}): Unexpected character '${s}', expected SPACE`,\n\t\t\t);\n\t\t}\n\n\t\tlet header = [];\n\t\twhile (true) {\n\t\t\tlet c = this.consumeChar();\n\t\t\tif (c == \"\\n\") {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\theader.push(c);\n\t\t}\n\t\treturn header.join(\"\");\n\t}\n\n\tprivate parseAliases(): string[] {\n\t\tlet asterisk;\n\t\tdo {\n\t\t\tasterisk = this.consumeChar();\n\t\t} while (asterisk == \"\\n\");\n\n\t\tif (asterisk != \"*\") {\n\t\t\t// aliases optional, so backtrack\n\t\t\tthis.spitChar();\n\t\t\treturn [];\n\t\t}\n\n\t\t// Consume until reach ASTERISK\n\t\tlet aliasStart = this.cursor;\n\t\tlet aliasEnd = aliasStart;\n\t\twhile (true) {\n\t\t\tlet c = this.consumeChar();\n\t\t\tif (c == \"\\n\") {\n\t\t\t\t// If we encounter a newline before a '*',\n\t\t\t\t// then determine that there is no alias declaration\n\t\t\t\tthis.cursor = aliasStart - 1;\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\tif (c == \"*\") {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\taliasEnd++;\n\t\t}\n\t\tlet aliasStr = this.fileContent.slice(aliasStart, aliasEnd);\n\t\tconst aliases = aliasStr.split(/[,|]/);\n\n\t\t// Continue consuming until newline (but all chars after the closing ASTERISK are ignored)\n\t\twhile (this.consumeChar() != \"\\n\") {}\n\n\t\treturn aliases.map((alias) => alias.trim());\n\t}\n\n\tprivate parseDef(): string {\n\t\tlet defStr = \"\";\n\n\t\twhile (true) {\n\t\t\tlet c = this.consumeChar();\n\t\t\tif (c === EOF) {\n\t\t\t\t// On EOF, treat all preceding chars as definition\n\t\t\t\treturn defStr;\n\t\t\t}\n\t\t\tdefStr += c;\n\t\t\tif (defStr.length >= 5) {\n\t\t\t\tif (this.checkDelimiter(defStr.slice(defStr.length - 5))) {\n\t\t\t\t\treturn defStr.slice(0, defStr.length - 5);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate checkDelimiter(d: string) {\n\t\tconst r = /\\n *((---)|(___)) *\\n/;\n\t\treturn r.test(d);\n\t}\n\n\t// For backtracking, used for optional grammars rules\n\tprivate spitChar(count?: number) {\n\t\tif (!count) {\n\t\t\tcount = 1;\n\t\t}\n\t\tfor (let i = 0; i < count; i++) {\n\t\t\tthis.cursor--;\n\t\t}\n\t}\n\n\tprivate consumeChar(): string {\n\t\tif (this.cursor >= this.fileContent.length) {\n\t\t\treturn EOF;\n\t\t}\n\t\tconst c = this.fileContent[this.cursor++];\n\t\tif (c === \"\\n\") {\n\t\t\tthis.currLine++;\n\t\t}\n\t\treturn c;\n\t}\n\n\tprivate headerToKey(key: string): string {\n\t\treturn this.parseSettings.enableCaseSensitive ? key : key.toLowerCase();\n\t}\n\n    private defBlockToDefinition(blk: DefblockAST): Definition {\n        return {\n            key: this.headerToKey(blk.header),\n            word: blk.header,\n            aliases: blk.aliases.concat(\n\t\t\t\tthis.calculatePlurals([blk.header].concat(blk.aliases)),\n\t\t\t),\n            definition: blk.body.trim(),\n            file: this.file,\n\t\t\tlinkText: `${this.file.path}${blk.header ? '#' + blk.header : ''}`,\n\t\t\tfileType: DefFileType.Consolidated,\n\t\t\tposition: {\n\t\t\t\tfrom: blk.position.from,\n\t\t\t\tto: blk.position.to,\n\t\t\t},\n\t\t};\n\t}\n}\n"
  },
  {
    "path": "src/core/def-file-manager.ts",
    "content": "import { App, TFile, TFolder } from \"obsidian\";\nimport { PTreeNode } from \"src/editor/prefix-tree\";\nimport { DEFAULT_DEF_FOLDER, VALID_DEFINITION_FILE_TYPES } from \"src/settings\";\nimport { normaliseWord } from \"src/util/editor\";\nimport { logDebug, logWarn } from \"src/util/log\";\nimport { useRetry } from \"src/util/retry\";\nimport { FileParser } from \"./file-parser\";\nimport { DefFileType } from \"./file-type\";\nimport { Definition } from \"./model\";\nimport { getSettings } from \"src/settings\";\n\nlet defFileManager: DefManager;\n\nexport const DEF_CTX_FM_KEY = \"def-context\";\n\nexport class DefManager {\n\tapp: App;\n\tglobalDefs: DefinitionRepo;\n\tglobalDefFolders: Map<string, TFolder>;\n\tglobalDefFiles: Map<string, TFile>;\n\tglobalPrefixTree: PTreeNode;\n\tlastUpdate: number;\n\n\tmarkedDirty: TFile[];\n\n\tconsolidatedDefFiles: Map<string, TFile>;\n\n\tactiveFile: TFile | null;\n\tlocalPrefixTree: PTreeNode;\n\tshouldUseLocal: boolean;\n\n\tlocalDefs: DefinitionRepo;\n\n\tconstructor(app: App) {\n\t\tthis.app = app;\n\t\tthis.globalDefs = new DefinitionRepo();\n\t\tthis.globalDefFiles = new Map<string, TFile>();\n\t\tthis.globalDefFolders = new Map<string, TFolder>();\n\t\tthis.globalPrefixTree = new PTreeNode();\n\t\tthis.consolidatedDefFiles = new Map<string, TFile>();\n\t\tthis.localDefs = new DefinitionRepo();\n\n\t\tthis.resetLocalConfigs();\n\t\tthis.lastUpdate = 0;\n\t\tthis.markedDirty = [];\n\n\t\tactiveWindow.NoteDefinition.definitions.global = this.globalDefs;\n\n\t\tthis.loadDefinitions();\n\t}\n\n\taddDefFile(file: TFile) {\n\t\tthis.globalDefFiles.set(file.path, file);\n\t}\n\n\t// Get the appropriate prefix tree to use for current active file\n\tgetPrefixTree() {\n\t\tif (this.shouldUseLocal) {\n\t\t\treturn this.localPrefixTree;\n\t\t}\n\t\treturn this.globalPrefixTree;\n\t}\n\n\t// Updates active file and rebuilds local prefix tree if necessary\n\tupdateActiveFile() {\n\t\tthis.activeFile = this.app.workspace.getActiveFile();\n\t\tthis.resetLocalConfigs();\n\n\t\tif (this.activeFile) {\n\t\t\tconst metadataCache = this.app.metadataCache.getFileCache(\n\t\t\t\tthis.activeFile,\n\t\t\t);\n\t\t\tif (!metadataCache) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst paths = metadataCache.frontmatter?.[DEF_CTX_FM_KEY];\n\t\t\tif (!paths) {\n\t\t\t\t// No def-source specified\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!Array.isArray(paths)) {\n\t\t\t\tlogWarn(\n\t\t\t\t\t`Unrecognised type for '${DEF_CTX_FM_KEY}' frontmatter`,\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst flattenedPaths = this.flattenPathList(paths);\n\t\t\tthis.buildLocalPrefixTree(flattenedPaths);\n\t\t\tthis.buildLocalDefRepo(flattenedPaths);\n\t\t\tthis.shouldUseLocal = true;\n\t\t}\n\t}\n\n\t// For manually updating definition sources, as metadata cache may not be the latest updated version\n\tupdateDefSources(defSource: string[]) {\n\t\tthis.resetLocalConfigs();\n\n\t\tif (!defSource || defSource.length === 0) {\n\t\t\treturn;\n\t\t}\n\t\tthis.buildLocalPrefixTree(defSource);\n\t\tthis.buildLocalDefRepo(defSource);\n\t\tthis.shouldUseLocal = true;\n\t}\n\n\tmarkDirty(file: TFile) {\n\t\tthis.markedDirty.push(file);\n\t}\n\n\tprivate flattenPathList(paths: string[]): string[] {\n\t\tconst filePaths: string[] = [];\n\t\tpaths.forEach((path) => {\n\t\t\tif (this.isFolderPath(path)) {\n\t\t\t\tfilePaths.push(...this.flattenFolder(path));\n\t\t\t} else {\n\t\t\t\tfilePaths.push(path);\n\t\t\t}\n\t\t});\n\t\treturn filePaths;\n\t}\n\n\t// Given a folder path, return an array of file paths\n\tprivate flattenFolder(path: string): string[] {\n\t\tif (path.endsWith(\"/\")) {\n\t\t\tpath = path.slice(0, path.length - 1);\n\t\t}\n\t\tconst folder = this.app.vault.getFolderByPath(path);\n\t\tif (!folder) {\n\t\t\treturn [];\n\t\t}\n\t\tconst childrenFiles = this.getChildrenFiles(folder);\n\t\treturn childrenFiles.map((file) => file.path);\n\t}\n\n\tprivate getChildrenFiles(folder: TFolder): TFile[] {\n\t\tconst files: TFile[] = [];\n\t\tfolder.children.forEach((abstractFile) => {\n\t\t\tif (abstractFile instanceof TFolder) {\n\t\t\t\tfiles.push(...this.getChildrenFiles(abstractFile));\n\t\t\t} else if (abstractFile instanceof TFile) {\n\t\t\t\tfiles.push(abstractFile);\n\t\t\t}\n\t\t});\n\t\treturn files;\n\t}\n\n\tprivate isFolderPath(path: string): boolean {\n\t\treturn path.endsWith(\"/\");\n\t}\n\n\t// Expects an array of file paths (not directories)\n\tprivate buildLocalPrefixTree(filePaths: string[]) {\n\t\tconst root = new PTreeNode();\n\t\tfilePaths.forEach((filePath) => {\n\t\t\tconst defMap = this.globalDefs.getMapForFile(filePath);\n\t\t\tif (!defMap) {\n\t\t\t\tlogWarn(`Unrecognised file path '${filePath}'`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t[...defMap.keys()].forEach((key) => {\n\t\t\t\troot.add(key, 0);\n\t\t\t});\n\t\t});\n\t\tthis.localPrefixTree = root;\n\t}\n\n\t// Expects an array of file paths (not directories)\n\tprivate buildLocalDefRepo(filePaths: string[]) {\n\t\tfilePaths.forEach((filePath) => {\n\t\t\tconst defMap = this.globalDefs.getMapForFile(filePath);\n\t\t\tif (defMap) {\n\t\t\t\tthis.localDefs.fileDefMap.set(filePath, defMap);\n\t\t\t}\n\t\t});\n\t}\n\n\tisDefFile(file: TFile): boolean {\n\t\treturn (\n\t\t\tfile.path.startsWith(this.getGlobalDefFolder()) &&\n\t\t\tVALID_DEFINITION_FILE_TYPES.some((ext) => file.path.endsWith(ext))\n\t\t);\n\t}\n\n\treset() {\n\t\tthis.globalPrefixTree = new PTreeNode();\n\t\tthis.globalDefs.clear();\n\t\tthis.globalDefFiles = new Map<string, TFile>();\n\t}\n\n\t// Load all definitions from registered def folder\n\t// This will recurse through the def folder, parsing all definition files\n\t// Expensive operation so use sparingly\n\tloadDefinitions() {\n\t\tthis.reset();\n\t\tthis.loadGlobals().then(this.updateActiveFile.bind(this));\n\t}\n\n\tprivate getDefRepo() {\n\t\treturn this.shouldUseLocal ? this.localDefs : this.globalDefs;\n\t}\n\n\tget(key: string) {\n\t\treturn this.getDefRepo().get(normaliseWord(key));\n\t}\n\n\tset(def: Definition) {\n\t\tthis.globalDefs.set(def);\n\t}\n\n\tgetDefFiles(): TFile[] {\n\t\treturn [...this.globalDefFiles.values()];\n\t}\n\n\tgetConsolidatedDefFiles(): TFile[] {\n\t\treturn [...this.consolidatedDefFiles.values()];\n\t}\n\n\tgetDefFolders(): TFolder[] {\n\t\treturn [...this.globalDefFolders.values()];\n\t}\n\n\tasync loadUpdatedFiles() {\n\t\tconst definitions: Definition[] = [];\n\t\tconst dirtyFiles: string[] = [];\n\n\t\tconst files = [...this.globalDefFiles.values(), ...this.markedDirty];\n\n\t\tfor (let file of files) {\n\t\t\tif (file.stat.mtime > this.lastUpdate) {\n\t\t\t\tlogDebug(\n\t\t\t\t\t`File ${file.path} was updated, reloading definitions...`,\n\t\t\t\t);\n\t\t\t\tdirtyFiles.push(file.path);\n\t\t\t\tconst defs = await this.parseFile(file);\n\t\t\t\tdefinitions.push(...defs);\n\t\t\t}\n\t\t}\n\n\t\tdirtyFiles.forEach((file) => {\n\t\t\tthis.globalDefs.clearForFile(file);\n\t\t});\n\n\t\tif (definitions.length > 0) {\n\t\t\tdefinitions.forEach((def) => {\n\t\t\t\tthis.globalDefs.set(def);\n\t\t\t});\n\t\t}\n\n\t\tthis.markedDirty = [];\n\t\tthis.buildPrefixTree();\n\t\tthis.lastUpdate = Date.now();\n\t}\n\n\t// Global configs should always be used by default\n\tprivate resetLocalConfigs() {\n\t\tthis.localPrefixTree = new PTreeNode();\n\t\tthis.shouldUseLocal = false;\n\t\tthis.localDefs.clear();\n\t}\n\n\tprivate async loadGlobals() {\n\t\tconst retry = useRetry();\n\t\tlet globalFolder: TFolder | null = null;\n\t\t// Retry is needed here as getFolderByPath may return null when being called on app startup\n\t\tawait retry.exec(() => {\n\t\t\tglobalFolder = this.app.vault.getFolderByPath(\n\t\t\t\tthis.getGlobalDefFolder(),\n\t\t\t);\n\t\t\tif (!globalFolder) {\n\t\t\t\tretry.setShouldRetry();\n\t\t\t}\n\t\t});\n\n\t\tif (!globalFolder) {\n\t\t\tlogWarn(\n\t\t\t\t\"Global definition folder not found, unable to load global definitions\",\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\t// Recursively load files within the global definition folder\n\t\tconst definitions = await this.parseFolder(globalFolder);\n\t\tdefinitions.forEach((def) => {\n\t\t\tthis.globalDefs.set(def);\n\t\t});\n\n\t\tthis.buildPrefixTree();\n\t\tthis.lastUpdate = Date.now();\n\t}\n\n\tprivate async buildPrefixTree() {\n\t\tconst root = new PTreeNode();\n\t\tthis.globalDefs.getAllKeys().forEach((key) => {\n\t\t\troot.add(key, 0);\n\t\t});\n\t\tthis.globalPrefixTree = root;\n\t}\n\n\tprivate async parseFolder(folder: TFolder): Promise<Definition[]> {\n\t\tthis.globalDefFolders.set(folder.path, folder);\n\t\tconst definitions: Definition[] = [];\n\t\tfor (let f of folder.children) {\n\t\t\tif (f instanceof TFolder) {\n\t\t\t\tlet defs = await this.parseFolder(f);\n\t\t\t\tdefinitions.push(...defs);\n\t\t\t} else if (f instanceof TFile && this.isDefFile(f)) {\n\t\t\t\tlet defs = await this.parseFile(f);\n\t\t\t\tdefinitions.push(...defs);\n\t\t\t}\n\t\t}\n\t\treturn definitions;\n\t}\n\n\tprivate async parseFile(file: TFile): Promise<Definition[]> {\n\t\tthis.globalDefFiles.set(file.path, file);\n\t\tlet parser = new FileParser(this.app, file);\n\t\tconst def = await parser.parseFile();\n\t\tif (parser.defFileType === DefFileType.Consolidated) {\n\t\t\tthis.consolidatedDefFiles.set(file.path, file);\n\t\t}\n\t\treturn def;\n\t}\n\n\t// Walk the definition directory to find definition files and folders\n\tgetDefFilesAndFolders(): [TFolder[], TFile[]] {\n\t\tconst parentDefFolder = this.app.vault.getFolderByPath(\n\t\t\tthis.getGlobalDefFolder(),\n\t\t);\n\t\tif (!parentDefFolder) {\n\t\t\tlogWarn(\"Failed to get parent def folder\");\n\t\t\treturn [[], []];\n\t\t}\n\t\treturn this.walkFolder(parentDefFolder);\n\t}\n\n\tprivate walkFolder(folder: TFolder): [TFolder[], TFile[]] {\n\t\tthis.globalDefFolders.set(folder.path, folder);\n\t\tconst folders = [folder];\n\t\tconst files = [];\n\t\tfor (let f of folder.children) {\n\t\t\tif (f instanceof TFolder) {\n\t\t\t\tconst [childFolders, childFiles] = this.walkFolder(f);\n\t\t\t\tfolders.push(...childFolders);\n\t\t\t\tfiles.push(...childFiles);\n\t\t\t} else if (f instanceof TFile && this.isDefFile(f)) {\n\t\t\t\tthis.globalDefFiles.set(f.path, f);\n\t\t\t\tfiles.push(f);\n\t\t\t}\n\t\t}\n\t\treturn [folders, files];\n\t}\n\n\tgetGlobalDefFolder() {\n\t\treturn window.NoteDefinition.settings.defFolder || DEFAULT_DEF_FOLDER;\n\t}\n}\n\nexport class DefinitionRepo {\n\t// file name -> {definition-key -> definition}\n\tfileDefMap: Map<string, Map<string, Definition>>;\n\n\tconstructor() {\n\t\tthis.fileDefMap = new Map<string, Map<string, Definition>>();\n\t}\n\n\tgetMapForFile(filePath: string) {\n\t\treturn this.fileDefMap.get(filePath);\n\t}\n\n\tget(key: string) {\n\t\tfor (let [_, defMap] of this.fileDefMap) {\n\t\t\tconst def = defMap.get(key);\n\t\t\tif (def) {\n\t\t\t\treturn def;\n\t\t\t}\n\t\t}\n\t}\n\n\tgetAllKeys(): string[] {\n\t\tconst keys: string[] = [];\n\t\tthis.fileDefMap.forEach((defMap, _) => {\n\t\t\tkeys.push(...defMap.keys());\n\t\t});\n\t\treturn keys;\n\t}\n\n\tset(def: Definition) {\n\t\tlet defMap = this.fileDefMap.get(def.file.path);\n\t\tif (!defMap) {\n\t\t\tdefMap = new Map<string, Definition>();\n\t\t\tthis.fileDefMap.set(def.file.path, defMap);\n\t\t}\n\t\t// Prefer the first encounter over subsequent collisions\n\t\tif (defMap.has(def.key)) {\n\t\t\treturn;\n\t\t}\n\t\tdefMap.set(def.key, def);\n\n\t\tif (def.aliases.length > 0) {\n\t\t\tdef.aliases.forEach((alias) => {\n\t\t\t\tif (defMap && getSettings().defFileParseConfig.enableCaseSensitive) {\n\t\t\t\t\tdefMap.set(alias, def);\n\t\t\t\t} else if (defMap) {\n\t\t\t\t\tdefMap.set(alias.toLowerCase(), def);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\n\tclearForFile(filePath: string) {\n\t\tconst defMap = this.fileDefMap.get(filePath);\n\t\tif (defMap) {\n\t\t\tdefMap.clear();\n\t\t}\n\t}\n\n\tclear() {\n\t\tthis.fileDefMap.clear();\n\t}\n}\n\nexport function initDefFileManager(app: App): DefManager {\n\tdefFileManager = new DefManager(app);\n\treturn defFileManager;\n}\n\nexport function getDefFileManager(): DefManager {\n\treturn defFileManager;\n}\n"
  },
  {
    "path": "src/core/def-file-updater.ts",
    "content": "import { App, Notice } from \"obsidian\";\nimport { getSettings } from \"src/settings\";\nimport { logError, logWarn } from \"src/util/log\";\nimport { getDefFileManager } from \"./def-file-manager\";\nimport { FileParser } from \"./file-parser\";\nimport { DefFileType } from \"./file-type\";\nimport { FrontmatterBuilder } from \"./fm-builder\";\nimport { Definition } from \"./model\";\n\nexport class DefFileUpdater {\n\tapp: App;\n\n\tconstructor(app: App) {\n\t\tthis.app = app;\n\t}\n\n\tasync updateDefinition(def: Definition) {\n\t\t// Ensure that key is case-insensitive\n\t\tdef.key = def.key.toLowerCase();\n\t\tdef.definition = def.definition.trim();\n\n\t\tif (def.fileType === DefFileType.Atomic) {\n\t\t\tawait this.updateAtomicDefFile(def);\n\t\t} else if (def.fileType === DefFileType.Consolidated) {\n\t\t\tawait this.updateConsolidatedDefFile(def);\n\t\t} else {\n\t\t\treturn;\n\t\t}\n\t\tawait getDefFileManager().loadUpdatedFiles();\n\t\tnew Notice(\"Definition successfully modified\");\n\t}\n\n\tprivate async updateAtomicDefFile(def: Definition) {\n\t\tawait this.app.vault.modify(def.file, def.definition);\n\t}\n\n\tprivate async updateConsolidatedDefFile(def: Definition) {\n\t\tconst file = def.file;\n\t\tconst fileContent = await this.app.vault.read(file);\n\n\t\tconst fileParser = new FileParser(this.app, file);\n\t\tconst defs = await fileParser.parseFile(fileContent);\n\n\t\tconst fileDef = defs.find((fileDef) => fileDef.key === def.key);\n\t\tif (!fileDef) {\n\t\t\tlogError(\"File definition not found, cannot edit\");\n\t\t\treturn;\n\t\t}\n\t\tif (!fileDef.position) {\n\t\t\tlogError(\"Position not set, cannot edit\");\n\t\t\treturn;\n\t\t}\n\n\t\t// Replace definition and aliases\n\t\tfileDef.definition = def.definition;\n\t\tfileDef.aliases = def.aliases;\n\n\t\t// account for frontmatter\n\t\tconst fileMetadata = this.app.metadataCache.getFileCache(file);\n\t\tconst fmPos = fileMetadata?.frontmatterPosition;\n\t\tlet fmContent: string = \"\";\n\t\tif (fmPos) {\n\t\t\tfmContent = fileContent.slice(0, fmPos.end.offset + 1);\n\t\t}\n\n\t\tconst newContent = this.generateConsDefFile(defs);\n\n\t\tawait this.app.vault.modify(file, fmContent + newContent);\n\t}\n\n\tasync addDefinition(def: Partial<Definition>, folder?: string) {\n\t\tdef.word = def.word?.trim();\n\t\tdef.definition = def.definition?.trim();\n\t\tif (!def.fileType) {\n\t\t\tlogError(\"File type missing\");\n\t\t\treturn;\n\t\t}\n\t\tif (def.fileType === DefFileType.Consolidated) {\n\t\t\tawait this.addConsolidatedFileDefinition(def);\n\t\t} else if (def.fileType === DefFileType.Atomic) {\n\t\t\tawait this.addAtomicFileDefinition(def, folder);\n\t\t}\n\t\tawait getDefFileManager().loadUpdatedFiles();\n\t\tnew Notice(\"Definition succesfully added\");\n\t}\n\n\tprivate async addAtomicFileDefinition(\n\t\tdef: Partial<Definition>,\n\t\tfolder?: string,\n\t) {\n\t\tif (!folder) {\n\t\t\tlogError(\"Folder missing for atomic file add\");\n\t\t\treturn;\n\t\t}\n\t\tif (!def.definition) {\n\t\t\tlogWarn(\"No definition given\");\n\t\t\treturn;\n\t\t}\n\t\tconst fmBuilder = new FrontmatterBuilder();\n\t\tfmBuilder.add(\"def-type\", \"atomic\");\n\t\tif (def.aliases) {\n\t\t\tconst aliases: string[] = [];\n\t\t\tdef.aliases.forEach((alias) => {\n\t\t\t\taliases.push(`- ${alias}`);\n\t\t\t});\n\t\t\tfmBuilder.add(\"aliases\", \"\\n\" + aliases.join(\"\\n\"));\n\t\t}\n\t\tconst fm = fmBuilder.finish();\n\t\tconst file = await this.app.vault.create(\n\t\t\t`${folder}/${def.word}.md`,\n\t\t\tfm + def.definition,\n\t\t);\n\n\t\tgetDefFileManager().addDefFile(file);\n\t\tgetDefFileManager().markDirty(file);\n\t}\n\n\tprivate async addConsolidatedFileDefinition(def: Partial<Definition>) {\n\t\tconst file = def.file;\n\t\tif (!file) {\n\t\t\tlogError(\"Add definition failed, no file given\");\n\t\t\treturn;\n\t\t}\n\t\tconst fileContent = await this.app.vault.read(file);\n\t\tconst fileParser = new FileParser(this.app, file);\n\t\tconst defs = await fileParser.parseFile(fileContent);\n\n\t\t// @ts-ignore: This is fine as long as word, alias (optional) and definition are present\n\t\t// Nothing else is used\n\t\tdefs.push(def);\n\n\t\t// account for frontmatter\n\t\tconst fileMetadata = this.app.metadataCache.getFileCache(file);\n\t\tconst fmPos = fileMetadata?.frontmatterPosition;\n\t\tlet fmContent: string = \"\";\n\t\tif (fmPos) {\n\t\t\tfmContent = fileContent.slice(0, fmPos.end.offset + 1);\n\t\t}\n\n\t\tconst newContent = this.generateConsDefFile(defs);\n\n\t\tawait this.app.vault.modify(file, fmContent + newContent);\n\t}\n\n\tprivate addSeparator(lines: string[]) {\n\t\tconst dividerSettings = getSettings().defFileParseConfig.divider;\n\t\tlet sepChoice = dividerSettings.underscore ? \"___\" : \"---\";\n\t\tlines.push(\"\", sepChoice, \"\");\n\t}\n\n\tprivate constructLinesFromDef(def: Partial<Definition>): string[] {\n\t\tconst lines = [`# ${def.word}`];\n\t\tif (def.aliases && def.aliases.length > 0) {\n\t\t\tconst aliasStr = `*${def.aliases.join(\", \")}*`;\n\t\t\tlines.push(\"\", aliasStr);\n\t\t}\n\t\tconst trimmedDef = def.definition\n\t\t\t? def.definition.replace(/\\s+$/g, \"\")\n\t\t\t: \"\";\n\t\tlines.push(\"\", trimmedDef);\n\t\treturn lines;\n\t}\n\n\t// Given an array of definitions, generate the contents of a consolidated definition file\n\t// Remember that this does not consider the frontmatter of a file\n\tprivate generateConsDefFile(defs: Definition[]): string {\n\t\tconst lines: string[] = [];\n\t\tdefs.forEach((def, idx) => {\n\t\t\tconst defLines = this.constructLinesFromDef(def);\n\t\t\tlines.push(...defLines);\n\t\t\tif (idx !== defs.length - 1) {\n\t\t\t\tthis.addSeparator(lines);\n\t\t\t}\n\t\t});\n\t\treturn lines.join(\"\\n\");\n\t}\n}\n"
  },
  {
    "path": "src/core/file-parser.ts",
    "content": "import { App, CachedMetadata, TFile } from \"obsidian\";\nimport { getSettings } from \"src/settings\";\nimport { useRetry } from \"src/util/retry\";\nimport { AtomicDefParser } from \"./atomic-def-parser\";\nimport { ConsolidatedDefParser } from \"./consolidated-def-parser\";\nimport { DefFileType } from \"./file-type\";\nimport { Definition } from \"./model\";\n\nexport const DEF_TYPE_FM = \"def-type\";\n\nexport class FileParser {\n\tapp: App;\n\tfile: TFile;\n\tdefFileType?: DefFileType;\n\n\tconstructor(app: App, file: TFile) {\n\t\tthis.app = app;\n\t\tthis.file = file;\n\t}\n\n\t// Optional argument used when file cache may not be updated\n\t// and we know the new contents of the file\n\tasync parseFile(fileContent?: string): Promise<Definition[]> {\n\t\tthis.defFileType = await this.getDefFileType();\n\n\t\tswitch (this.defFileType) {\n\t\t\tcase DefFileType.Consolidated:\n\t\t\t\tconst defParser = new ConsolidatedDefParser(\n\t\t\t\t\tthis.app,\n\t\t\t\t\tthis.file,\n\t\t\t\t);\n\t\t\t\treturn defParser.parseFile(fileContent);\n\t\t\tcase DefFileType.Atomic:\n\t\t\t\tconst atomicParser = new AtomicDefParser(this.app, this.file);\n\t\t\t\treturn atomicParser.parseFile(fileContent);\n\t\t}\n\t}\n\n\tprivate async getDefFileType(): Promise<DefFileType> {\n\t\tlet fileCache: CachedMetadata | null = null;\n\t\t// fileCache may return nil at on Obsidian startup. Obsidian likely needs some time to warm up the cache\n\t\tconst retry = useRetry();\n\t\tawait retry.exec(() => {\n\t\t\tfileCache = this.app.metadataCache.getFileCache(this.file);\n\t\t\tif (!fileCache) {\n\t\t\t\tretry.setShouldRetry();\n\t\t\t}\n\t\t});\n\t\t// @ts-ignore: fileCache should be set in the closure above\n\t\tconst fmFileType = fileCache?.frontmatter?.[DEF_TYPE_FM];\n\t\tif (\n\t\t\tfmFileType &&\n\t\t\t(fmFileType === DefFileType.Consolidated ||\n\t\t\t\tfmFileType === DefFileType.Atomic)\n\t\t) {\n\t\t\treturn fmFileType;\n\t\t}\n\n\t\t// Fallback to configured default\n\t\tconst parserSettings = getSettings().defFileParseConfig;\n\n\t\tif (parserSettings.defaultFileType) {\n\t\t\treturn parserSettings.defaultFileType;\n\t\t}\n\t\treturn DefFileType.Consolidated;\n\t}\n}\n"
  },
  {
    "path": "src/core/file-type.ts",
    "content": "export enum DefFileType {\n\tConsolidated = \"consolidated\",\n\tAtomic = \"atomic\",\n}\n"
  },
  {
    "path": "src/core/fm-builder.ts",
    "content": "export class FrontmatterBuilder {\n\tfm: Map<string, string>;\n\n\tconstructor() {\n\t\tthis.fm = new Map<string, string>();\n\t}\n\n\tadd(k: string, v: string) {\n\t\tthis.fm.set(k, v);\n\t}\n\n\tfinish(): string {\n\t\tlet fm = \"---\\n\";\n\t\tthis.fm.forEach((v, k) => {\n\t\t\tfm += `${k}: ${v}\\n`;\n\t\t});\n\t\tfm += \"---\\n\";\n\t\treturn fm;\n\t}\n}\n"
  },
  {
    "path": "src/core/model.ts",
    "content": "import { TFile } from \"obsidian\";\nimport { DefFileType } from \"./file-type\";\n\nexport interface Definition {\n\tkey: string;\n\tword: string;\n\taliases: string[];\n\tdefinition: string;\n\tfile: TFile;\n\tlinkText: string;\n\tfileType: DefFileType;\n\tposition?: FilePosition;\n}\n\n// Both to and from inclusive\nexport interface FilePosition {\n\tfrom: number;\n\tto: number;\n}\n"
  },
  {
    "path": "src/editor/add-modal.ts",
    "content": "import {\n\tApp,\n\tDropdownComponent,\n\tModal,\n\tNotice,\n\tSetting,\n\tTFile,\n} from \"obsidian\";\nimport { getDefFileManager, DEF_CTX_FM_KEY } from \"src/core/def-file-manager\";\nimport { DefFileUpdater } from \"src/core/def-file-updater\";\nimport { DefFileType } from \"src/core/file-type\";\n\nexport class AddDefinitionModal {\n\tapp: App;\n\tactiveFile: TFile | null;\n\tmodal: Modal;\n\taliases: string;\n\tdefinition: string;\n\tsubmitting: boolean;\n\n\tfileTypePicker: DropdownComponent;\n\tdefFilePickerSetting: Setting;\n\tdefFilePicker: DropdownComponent;\n\n\tatomicFolderPickerSetting: Setting;\n\tatomicFolderPicker: DropdownComponent;\n\n\tconstructor(app: App) {\n\t\tthis.app = app;\n\t\tthis.modal = new Modal(app);\n\t}\n\n\tasync open(text?: string) {\n\t\t// initialize the view when the modal is opened to ensure it's up to date\n\t\tthis.activeFile = this.app.workspace.getActiveFile();\n\n\t\tthis.submitting = false;\n\n\t\t// create modal content\n\t\tthis.modal.setTitle(\"Add Definition\");\n\t\tthis.modal.contentEl.createDiv({\n\t\t\tcls: \"edit-modal-section-header\",\n\t\t\ttext: \"Word/Phrase\",\n\t\t});\n\t\tconst phraseText = this.modal.contentEl.createEl(\"textarea\", {\n\t\t\tcls: \"edit-modal-aliases\",\n\t\t\tattr: {\n\t\t\t\tplaceholder: \"Word/phrase to be defined\",\n\t\t\t},\n\t\t\ttext: text ?? \"\",\n\t\t});\n\t\tthis.modal.contentEl.createDiv({\n\t\t\tcls: \"edit-modal-section-header\",\n\t\t\ttext: \"Aliases\",\n\t\t});\n\t\tconst aliasText = this.modal.contentEl.createEl(\"textarea\", {\n\t\t\tcls: \"edit-modal-aliases\",\n\t\t\tattr: {\n\t\t\t\tplaceholder: \"Add comma-separated aliases here\",\n\t\t\t},\n\t\t});\n\t\tthis.modal.contentEl.createDiv({\n\t\t\tcls: \"edit-modal-section-header\",\n\t\t\ttext: \"Definition\",\n\t\t});\n\t\tconst defText = this.modal.contentEl.createEl(\"textarea\", {\n\t\t\tcls: \"edit-modal-textarea\",\n\t\t\tattr: {\n\t\t\t\tplaceholder: \"Add definition here\",\n\t\t\t},\n\t\t});\n\n\t\t// create definition file picker\n\t\tconst defManager = getDefFileManager();\n\n\t\t// Get the most updated def files and folders on modal open\n\t\tlet [defFolders, defFiles] = defManager.getDefFilesAndFolders();\n\n\t\tif (defFolders.length === 0) {\n\t\t\tawait this.app.vault.createFolder(defManager.getGlobalDefFolder());\n\t\t\tawait this.app.vault.create(\n\t\t\t\t`${defManager.getGlobalDefFolder()}/definitions.md`,\n\t\t\t\t\"\",\n\t\t\t);\n\t\t\tconst f = defManager.getDefFilesAndFolders();\n\t\t\tdefFolders = f[0];\n\t\t\tdefFiles = f[1];\n\t\t}\n\n\t\t// get the currently opened file's first folder and first file, if they exist\n\t\tlet default_def_file = \"\";\n\t\tlet default_def_folder = \"\";\n\t\tlet paths: Array<string> = [];\n\n\t\t// if the currently open file has at least one definition context, use it's\n\t\t// first context as the initial value\n\t\tif (this.activeFile) {\n\t\t\tconst metadataCache = this.app.metadataCache.getFileCache(\n\t\t\t\tthis.activeFile,\n\t\t\t);\n\t\t\tpaths = metadataCache?.frontmatter?.[DEF_CTX_FM_KEY];\n\t\t\tif (paths) {\n\t\t\t\t// get the first folder in the path (if it exists) - use regexp to remove the trailing\n\t\t\t\t// `/` that might be present\n\t\t\t\tdefault_def_folder =\n\t\t\t\t\tpaths.find(\n\t\t\t\t\t\t(path: string) =>\n\t\t\t\t\t\t\tthis.app.vault.getFolderByPath(\n\t\t\t\t\t\t\t\tpath.replace(/\\/+$/, \"\"),\n\t\t\t\t\t\t\t) != null,\n\t\t\t\t\t) || \"\";\n\t\t\t\tif (default_def_folder) {\n\t\t\t\t\tdefault_def_folder = default_def_folder.replace(/\\/+$/, \"\");\n\t\t\t\t}\n\n\t\t\t\t// get the first file in the path (if it exists)\n\t\t\t\tdefault_def_file =\n\t\t\t\t\tpaths.find(\n\t\t\t\t\t\t(path: string) =>\n\t\t\t\t\t\t\tthis.app.vault.getFileByPath(path) != null,\n\t\t\t\t\t) || \"\";\n\t\t\t}\n\t\t}\n\n\t\tthis.defFilePickerSetting = new Setting(this.modal.contentEl)\n\t\t\t.setName(\"Definition file\")\n\t\t\t.addDropdown((component) => {\n\t\t\t\tdefFiles.forEach((file) => {\n\t\t\t\t\tcomponent.addOption(file.path, file.path);\n\t\t\t\t});\n\n\t\t\t\t// use the first definition file from this file's metadata, or default to\n\t\t\t\t// the first consolidated def file in the list if it exists\n\t\t\t\tif (default_def_file) {\n\t\t\t\t\tcomponent.setValue(default_def_file);\n\t\t\t\t} else if (defFiles.length > 0) {\n\t\t\t\t\tcomponent.setValue(defFiles[0].path);\n\t\t\t\t}\n\n\t\t\t\tthis.defFilePicker = component;\n\t\t\t});\n\n\t\tthis.atomicFolderPickerSetting = new Setting(this.modal.contentEl)\n\t\t\t.setName(\"Add file to folder\")\n\t\t\t.addDropdown((component) => {\n\t\t\t\tdefFolders.forEach((folder) => {\n\t\t\t\t\tcomponent.addOption(folder.path, folder.path + \"/\");\n\t\t\t\t});\n\n\t\t\t\t// use the first definition folder from this file's metadata, or default to\n\t\t\t\t// the first folder in the list if it exists\n\t\t\t\tif (default_def_folder) {\n\t\t\t\t\tcomponent.setValue(default_def_folder);\n\t\t\t\t} else if (defFolders.length > 0) {\n\t\t\t\t\tcomponent.setValue(defFolders[0].path);\n\t\t\t\t}\n\n\t\t\t\tthis.atomicFolderPicker = component;\n\t\t\t});\n\n\t\tnew Setting(this.modal.contentEl)\n\t\t\t.setName(\"Definition file type\")\n\t\t\t.addDropdown((component) => {\n\t\t\t\tconst handleDefFileTypeChange = (val: string) => {\n\t\t\t\t\tif (val === DefFileType.Consolidated) {\n\t\t\t\t\t\tthis.atomicFolderPickerSetting.settingEl.hide();\n\t\t\t\t\t\tthis.defFilePickerSetting.settingEl.show();\n\t\t\t\t\t} else if (val === DefFileType.Atomic) {\n\t\t\t\t\t\tthis.defFilePickerSetting.settingEl.hide();\n\t\t\t\t\t\tthis.atomicFolderPickerSetting.settingEl.show();\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\tcomponent.addOption(DefFileType.Consolidated, \"Consolidated\");\n\t\t\t\tcomponent.addOption(DefFileType.Atomic, \"Atomic\");\n\n\t\t\t\t// use the default definition type as a fallback\n\t\t\t\tcomponent.setValue(\n\t\t\t\t\twindow.NoteDefinition.settings.defFileParseConfig\n\t\t\t\t\t\t.defaultFileType,\n\t\t\t\t);\n\n\t\t\t\tcomponent.onChange(handleDefFileTypeChange);\n\t\t\t\thandleDefFileTypeChange(component.getValue());\n\t\t\t\tthis.fileTypePicker = component;\n\t\t\t});\n\n\t\tconst button = this.modal.contentEl.createEl(\"button\", {\n\t\t\ttext: \"Save\",\n\t\t\tcls: \"edit-modal-save-button\",\n\t\t});\n\n\t\tbutton.addEventListener(\"click\", () => {\n\t\t\tthis.try_submit(phraseText, defText, aliasText);\n\t\t});\n\n\t\t// set up key event listeners for closing and submitting the modal\n\t\tthis.modal.scope.register([\"Mod\"], \"Enter\", () => {\n\t\t\tthis.try_submit(phraseText, defText, aliasText);\n\t\t});\n\n\t\tthis.modal.open();\n\t}\n\n\t// Checks if the requirements for a definition (name, description, file) have been met,\n\t// showing an error notification if they haven't. Creates the definition and closes the modal\n\t// if there aren't any issues.\n\ttry_submit(\n\t\tphraseText: HTMLTextAreaElement,\n\t\tdefText: HTMLTextAreaElement,\n\t\taliasText: HTMLTextAreaElement,\n\t) {\n\t\t// we're already submitting the definition\n\t\tif (this.submitting) {\n\t\t\treturn;\n\t\t}\n\n\t\t// invalid definition paramters (missing name or description)\n\t\tif (!phraseText.value || !defText.value) {\n\t\t\tnew Notice(\"Please fill in a definition value\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst fileType = this.fileTypePicker.getValue();\n\t\tlet selectedPath = \"\";\n\t\tlet definitionFile;\n\n\t\tif (fileType === DefFileType.Consolidated) {\n\t\t\tselectedPath = this.defFilePicker.getValue();\n\t\t\tif (!selectedPath) {\n\t\t\t\tnew Notice(\n\t\t\t\t\t\"Please choose a definition file. If you do not have any definition files, please create one.\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst defFileManager = getDefFileManager();\n\t\t\tdefinitionFile = defFileManager.globalDefFiles.get(selectedPath);\n\t\t} else if (fileType === DefFileType.Atomic) {\n\t\t\tselectedPath = this.atomicFolderPicker.getValue();\n\t\t\tif (!selectedPath) {\n\t\t\t\tnew Notice(\"Please choose a folder for the atomic definition.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tdefinitionFile = undefined;\n\t\t} else {\n\t\t\tnew Notice(\"Invalid file type selected.\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst updated = new DefFileUpdater(this.app);\n\t\tupdated.addDefinition(\n\t\t\t{\n\t\t\t\tfileType: fileType as DefFileType,\n\t\t\t\tkey: phraseText.value.toLowerCase(),\n\t\t\t\tword: phraseText.value,\n\t\t\t\taliases: aliasText.value\n\t\t\t\t\t? aliasText.value.split(\",\").map((alias) => alias.trim())\n\t\t\t\t\t: [],\n\t\t\t\tdefinition: defText.value,\n\t\t\t\tfile: definitionFile,\n\t\t\t},\n\t\t\tselectedPath,\n\t\t);\n\t\tthis.modal.close();\n\t}\n}\n"
  },
  {
    "path": "src/editor/common.ts",
    "content": "import { Platform } from \"obsidian\";\nimport { getSettings, PopoverEventSettings } from \"src/settings\";\n\nconst triggerFunc =\n\t\"event.stopPropagation();activeWindow.NoteDefinition.triggerDefPreview(this);\";\n\nexport const DEF_DECORATION_CLS = \"def-decoration\";\n\n// For normal decoration of definitions\nexport function getDecorationAttrs(phrase: string): { [key: string]: string } {\n\tconst attributes: { [key: string]: string } = {\n\t\tdef: phrase,\n\t};\n\tconst settings = getSettings();\n\tif (Platform.isMobile) {\n\t\tattributes.onclick = triggerFunc;\n\t\treturn attributes;\n\t}\n\tif (settings.popoverEvent === PopoverEventSettings.Click) {\n\t\tattributes.onclick = triggerFunc;\n\t} else {\n\t\tattributes.onmouseenter = triggerFunc;\n\t}\n\tif (!settings.enableSpellcheck) {\n\t\tattributes.spellcheck = \"false\";\n\t}\n\treturn attributes;\n}\n"
  },
  {
    "path": "src/editor/decoration.ts",
    "content": "import { RangeSetBuilder } from \"@codemirror/state\";\nimport {\n\tDecoration,\n\tDecorationSet,\n\tEditorView,\n\tPluginSpec,\n\tPluginValue,\n\tViewPlugin,\n\tViewUpdate,\n} from \"@codemirror/view\";\nimport { logDebug } from \"src/util/log\";\nimport { DEF_DECORATION_CLS, getDecorationAttrs } from \"./common\";\nimport { LineScanner } from \"./definition-search\";\nimport { PTreeNode } from \"./prefix-tree\";\n\n// Information of phrase that can be used to add decorations within the editor\ninterface PhraseInfo {\n\tfrom: number;\n\tto: number;\n\tphrase: string;\n}\n\nlet markedPhrases: PhraseInfo[] = [];\n\nexport function getMarkedPhrases(): PhraseInfo[] {\n\treturn markedPhrases;\n}\n\n// View plugin to mark definitions\nexport class DefinitionMarker implements PluginValue {\n\tdecorations: DecorationSet;\n\teditorView: EditorView;\n\n\tconstructor(view: EditorView) {\n\t\tthis.editorView = view;\n\t\tthis.decorations = this.buildDecorations(view);\n\t}\n\n\tupdate(update: ViewUpdate) {\n\t\tif (\n\t\t\tupdate.docChanged ||\n\t\t\tupdate.viewportChanged ||\n\t\t\tupdate.focusChanged\n\t\t) {\n\t\t\tconst start = performance.now();\n\t\t\tthis.decorations = this.buildDecorations(update.view);\n\t\t\tconst end = performance.now();\n\t\t\tlogDebug(`Marked definitions in ${end - start}ms`);\n\t\t\treturn;\n\t\t}\n\t}\n\n\tpublic forceUpdate() {\n\t\tconst start = performance.now();\n\t\tthis.decorations = this.buildDecorations(this.editorView);\n\t\tconst end = performance.now();\n\t\tlogDebug(`Marked definitions in ${end - start}ms`);\n\t\treturn;\n\t}\n\n\tdestroy() {}\n\n\tbuildDecorations(view: EditorView): DecorationSet {\n\t\tconst builder = new RangeSetBuilder<Decoration>();\n\t\tconst phraseInfos: PhraseInfo[] = [];\n\n\t\tfor (let { from, to } of view.visibleRanges) {\n\t\t\tconst text = view.state.sliceDoc(from, to);\n\t\t\tphraseInfos.push(...scanText(text, from));\n\t\t}\n\n\t\tphraseInfos.forEach((wordPos) => {\n\t\t\tconst attributes = getDecorationAttrs(wordPos.phrase);\n\t\t\tbuilder.add(\n\t\t\t\twordPos.from,\n\t\t\t\twordPos.to,\n\t\t\t\tDecoration.mark({\n\t\t\t\t\tclass: DEF_DECORATION_CLS,\n\t\t\t\t\tattributes: attributes,\n\t\t\t\t}),\n\t\t\t);\n\t\t});\n\n\t\tmarkedPhrases = phraseInfos;\n\t\treturn builder.finish();\n\t}\n}\n\n// Scan text and return phrases and their positions that require decoration\nexport function scanText(\n\ttext: string,\n\toffset: number,\n\tpTree?: PTreeNode,\n): PhraseInfo[] {\n\tlet phraseInfos: PhraseInfo[] = [];\n\tconst lines = text.split(/\\r?\\n/);\n\tlet internalOffset = offset;\n\tconst lineScanner = new LineScanner(pTree);\n\n\tlines.forEach((line) => {\n\t\tphraseInfos.push(...lineScanner.scanLine(line, internalOffset));\n\t\t// Additional 1 char for \\n char\n\t\tinternalOffset += line.length + 1;\n\t});\n\n\t// Decorations need to be sorted by 'from' ascending, then 'to' descending\n\t// This allows us to prefer longer words over shorter ones\n\tphraseInfos.sort((a, b) => b.to - a.to);\n\tphraseInfos.sort((a, b) => a.from - b.from);\n\treturn removeSubsetsAndIntersects(phraseInfos);\n}\n\nfunction removeSubsetsAndIntersects(phraseInfos: PhraseInfo[]): PhraseInfo[] {\n\tlet cursor = 0;\n\treturn phraseInfos.filter((phraseInfo) => {\n\t\tif (phraseInfo.from >= cursor) {\n\t\t\tcursor = phraseInfo.to;\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t});\n}\n\nconst pluginSpec: PluginSpec<DefinitionMarker> = {\n\tdecorations: (value: DefinitionMarker) => value.decorations,\n};\n\nexport const definitionMarker = ViewPlugin.fromClass(\n\tDefinitionMarker,\n\tpluginSpec,\n);\n"
  },
  {
    "path": "src/editor/def-file-registration.ts",
    "content": "import { App, TFile } from \"obsidian\";\nimport { getDefFileManager } from \"src/core/def-file-manager\";\nimport { DEF_TYPE_FM } from \"src/core/file-parser\";\nimport { DefFileType } from \"src/core/file-type\";\nimport { logError } from \"src/util/log\";\n\nexport function registerDefFile(app: App, file: TFile, fileType: DefFileType) {\n\tapp.fileManager\n\t\t.processFrontMatter(file, (fm) => {\n\t\t\tfm[DEF_TYPE_FM] = fileType;\n\t\t\tgetDefFileManager().loadDefinitions();\n\t\t})\n\t\t.catch((e) => {\n\t\t\tlogError(`Err writing to frontmatter of file: ${e}`);\n\t\t});\n}\n"
  },
  {
    "path": "src/editor/definition-popover.ts",
    "content": "import {\n\tApp,\n\tButtonComponent,\n\tComponent,\n\tMarkdownRenderer,\n\tMarkdownView,\n\tnormalizePath,\n\tPlugin,\n} from \"obsidian\";\nimport { Definition } from \"src/core/model\";\nimport { getSettings, PopoverDismissType } from \"src/settings\";\nimport { logDebug, logError } from \"src/util/log\";\n\nconst DEF_POPOVER_ID = \"definition-popover\";\n\nlet definitionPopover: DefinitionPopover;\n\ninterface Coordinates {\n\tleft: number;\n\tright: number;\n\ttop: number;\n\tbottom: number;\n}\n\nexport class DefinitionPopover extends Component {\n\tapp: App;\n\tplugin: Plugin;\n\t// Code mirror editor object for capturing vim events\n\tcmEditor: any;\n\t// Ref to the currently mounted popover\n\t// There should only be one mounted popover at all times\n\tmountedPopover: HTMLElement | undefined;\n\n\tconstructor(plugin: Plugin) {\n\t\tsuper();\n\t\tthis.app = plugin.app;\n\t\tthis.plugin = plugin;\n\t\tthis.cmEditor = this.getCmEditor(this.app);\n\t}\n\n\t// Open at editor cursor's position\n\topenAtCursor(def: Definition) {\n\t\tthis.unmount();\n\t\tthis.mountAtCursor(def);\n\n\t\tif (!this.mountedPopover) {\n\t\t\tlogError(\"Mounting definition popover failed\");\n\t\t\treturn;\n\t\t}\n\n\t\tthis.registerClosePopoverListeners();\n\t}\n\n\t// Open at coordinates (can use for opening at mouse position)\n\topenAtCoords(def: Definition, coords: Coordinates) {\n\t\tthis.unmount();\n\t\tthis.mountAtCoordinates(def, coords);\n\n\t\tif (!this.mountedPopover) {\n\t\t\tlogError(\"mounting definition popover failed\");\n\t\t\treturn;\n\t\t}\n\t\tthis.registerClosePopoverListeners();\n\t}\n\n\tcleanUp() {\n\t\tlogDebug(\"Cleaning popover elements\");\n\t\tconst popoverEls = document.getElementsByClassName(DEF_POPOVER_ID);\n\t\tfor (let i = 0; i < popoverEls.length; i++) {\n\t\t\tpopoverEls[i].remove();\n\t\t}\n\t}\n\n\tclose = () => {\n\t\tthis.unmount();\n\t};\n\n\tclickClose = () => {\n\t\tif (this.mountedPopover?.matches(\":hover\")) {\n\t\t\treturn;\n\t\t}\n\t\tthis.close();\n\t};\n\n\tprivate getCmEditor(app: App) {\n\t\tconst activeView = app.workspace.getActiveViewOfType(MarkdownView);\n\t\tconst cmEditor = (activeView as any)?.editMode?.editor?.cm?.cm;\n\t\tif (!cmEditor) {\n\t\t\tlogDebug(\n\t\t\t\t\"cmEditor object not found, will not handle vim events for definition popover\",\n\t\t\t);\n\t\t}\n\t\treturn cmEditor;\n\t}\n\n\tprivate shouldOpenToLeft(\n\t\thorizontalOffset: number,\n\t\tcontainerStyle: CSSStyleDeclaration,\n\t): boolean {\n\t\treturn horizontalOffset > parseInt(containerStyle.width) / 2;\n\t}\n\n\tprivate shouldOpenUpwards(\n\t\tverticalOffset: number,\n\t\tcontainerStyle: CSSStyleDeclaration,\n\t): boolean {\n\t\treturn verticalOffset > parseInt(containerStyle.height) / 2;\n\t}\n\n\t// Creates popover element and its children, without displaying it\n\tprivate createElement(\n\t\tdef: Definition,\n\t\tparent: HTMLElement,\n\t): HTMLDivElement {\n\t\tconst popoverSettings = getSettings().defPopoverConfig;\n\t\tconst el = parent.createEl(\"div\", {\n\t\t\tcls: \"definition-popover\",\n\t\t\tattr: {\n\t\t\t\tid: DEF_POPOVER_ID,\n\t\t\t\tstyle: `visibility:hidden;${\n\t\t\t\t\tpopoverSettings.backgroundColour\n\t\t\t\t\t\t? `background-color: ${popoverSettings.backgroundColour};`\n\t\t\t\t\t\t: \"\"\n\t\t\t\t}`,\n\t\t\t},\n\t\t});\n\n\t\t// create a button linking to the definition\n\t\tnew ButtonComponent(el)\n\t\t\t.setIcon(\"arrow-left-from-line\")\n\t\t\t.setTooltip(\"Go to definition\")\n\t\t\t.setClass(\"popover-go-to-def-button\")\n\t\t\t.onClick(() => {\n\t\t\t\tthis.app.workspace.openLinkText(def.linkText, \"\");\n\t\t\t});\n\n\t\tel.createEl(\"h2\", { text: def.word });\n\t\tif (def.aliases.length > 0 && popoverSettings.displayAliases) {\n\t\t\tel.createEl(\"i\", { text: def.aliases.join(\", \") });\n\t\t}\n\t\tconst contentEl = el.createEl(\"div\");\n\t\tcontentEl.setAttr(\"ctx\", \"def-popup\");\n\n\t\tconst currComponent = this;\n\t\tMarkdownRenderer.render(\n\t\t\tthis.app,\n\t\t\tdef.definition,\n\t\t\tcontentEl,\n\t\t\tnormalizePath(def.file.path),\n\t\t\tcurrComponent,\n\t\t);\n\t\tthis.postprocessMarkdown(contentEl, def);\n\n\t\tif (popoverSettings.displayDefFileName) {\n\t\t\tel.createEl(\"div\", {\n\t\t\t\ttext: def.file.basename,\n\t\t\t\tcls: \"definition-popover-filename\",\n\t\t\t});\n\t\t}\n\t\treturn el;\n\t}\n\n\t// Internal links do not work properly in the popover\n\t// This is to manually open internal links\n\tprivate postprocessMarkdown(el: HTMLDivElement, def: Definition) {\n\t\tconst internalLinks = el.getElementsByClassName(\"internal-link\");\n\t\tfor (let i = 0; i < internalLinks.length; i++) {\n\t\t\tconst linkEl = internalLinks.item(i);\n\t\t\tif (linkEl) {\n\t\t\t\tlinkEl.addEventListener(\"click\", (e) => {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tconst file = this.app.metadataCache.getFirstLinkpathDest(\n\t\t\t\t\t\tlinkEl.getAttr(\"href\") ?? \"\",\n\t\t\t\t\t\tnormalizePath(def.file.path),\n\t\t\t\t\t);\n\t\t\t\t\tthis.unmount();\n\t\t\t\t\tif (!file) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.app.workspace.getLeaf().openFile(file);\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate mountAtCursor(def: Definition) {\n\t\tlet cursorCoords;\n\t\ttry {\n\t\t\tcursorCoords = this.getCursorCoords();\n\t\t} catch (e) {\n\t\t\tlogError(\n\t\t\t\t\"Could not open definition popover - could not get cursor coordinates\",\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.mountAtCoordinates(def, cursorCoords);\n\t}\n\n\t// Offset coordinates from viewport coordinates to coordinates relative to the parent container element\n\tprivate offsetCoordsToContainer(\n\t\tcoords: Coordinates,\n\t\tcontainer: HTMLElement,\n\t): Coordinates {\n\t\tconst containerRect = container.getBoundingClientRect();\n\t\treturn {\n\t\t\tleft: coords.left - containerRect.left,\n\t\t\tright: coords.right - containerRect.left,\n\t\t\ttop: coords.top - containerRect.top,\n\t\t\tbottom: coords.bottom - containerRect.top,\n\t\t};\n\t}\n\n\tprivate mountAtCoordinates(def: Definition, coords: Coordinates) {\n\t\tconst mdView = this.app.workspace.getActiveViewOfType(MarkdownView);\n\t\tif (!mdView) {\n\t\t\tlogError(\"Could not mount popover: No active markdown view found\");\n\t\t\treturn;\n\t\t}\n\n\t\tthis.mountedPopover = this.createElement(def, mdView.containerEl);\n\t\tthis.positionAndSizePopover(mdView, coords);\n\t}\n\n\t// Position and display popover\n\tprivate positionAndSizePopover(mdView: MarkdownView, coords: Coordinates) {\n\t\tif (!this.mountedPopover) {\n\t\t\treturn;\n\t\t}\n\t\tconst popoverSettings = getSettings().defPopoverConfig;\n\t\tconst containerStyle = getComputedStyle(mdView.containerEl);\n\t\tconst matchedClasses =\n\t\t\tmdView.containerEl.getElementsByClassName(\"view-header\");\n\t\t// The container div has a header element that needs to be accounted for\n\t\tlet offsetHeaderHeight = 0;\n\t\tif (matchedClasses.length > 0) {\n\t\t\toffsetHeaderHeight = parseInt(\n\t\t\t\tgetComputedStyle(matchedClasses[0]).height,\n\t\t\t);\n\t\t}\n\n\t\t// Offset coordinates to be relative to container\n\t\tcoords = this.offsetCoordsToContainer(coords, mdView.containerEl);\n\n\t\tconst positionStyle: Partial<CSSStyleDeclaration> = {\n\t\t\tvisibility: \"visible\",\n\t\t};\n\n\t\tpositionStyle.maxWidth =\n\t\t\tpopoverSettings.enableCustomSize && popoverSettings.maxWidth\n\t\t\t\t? `${popoverSettings.maxWidth}px`\n\t\t\t\t: `${parseInt(containerStyle.width) / 2}px`;\n\t\tif (this.shouldOpenToLeft(coords.left, containerStyle)) {\n\t\t\tpositionStyle.right = `${parseInt(containerStyle.width) - coords.right}px`;\n\t\t} else {\n\t\t\tpositionStyle.left = `${coords.left}px`;\n\t\t}\n\n\t\tif (this.shouldOpenUpwards(coords.top, containerStyle)) {\n\t\t\tpositionStyle.bottom = `${parseInt(containerStyle.height) - coords.top}px`;\n\t\t\tpositionStyle.maxHeight =\n\t\t\t\tpopoverSettings.enableCustomSize && popoverSettings.maxHeight\n\t\t\t\t\t? `${popoverSettings.maxHeight}px`\n\t\t\t\t\t: `${coords.top - offsetHeaderHeight}px`;\n\t\t} else {\n\t\t\tpositionStyle.top = `${coords.bottom}px`;\n\t\t\tpositionStyle.maxHeight =\n\t\t\t\tpopoverSettings.enableCustomSize && popoverSettings.maxHeight\n\t\t\t\t\t? `${popoverSettings.maxHeight}px`\n\t\t\t\t\t: `${parseInt(containerStyle.height) - coords.bottom}px`;\n\t\t}\n\n\t\tthis.mountedPopover.setCssStyles(positionStyle);\n\t}\n\n\tprivate unmount() {\n\t\tif (!this.mountedPopover) {\n\t\t\tlogDebug(\"Nothing to unmount, could not find popover element\");\n\t\t\treturn;\n\t\t}\n\t\tthis.mountedPopover.remove();\n\t\tthis.mountedPopover = undefined;\n\n\t\tthis.unregisterClosePopoverListeners();\n\t}\n\n\t// This uses internal non-exposed codemirror API to get cursor coordinates\n\t// Cursor coordinates seem to be relative to viewport\n\tprivate getCursorCoords(): Coordinates {\n\t\tconst editor = this.app.workspace.activeEditor?.editor;\n\t\t// @ts-ignore\n\t\treturn editor?.cm?.coordsAtPos(\n\t\t\teditor?.posToOffset(editor?.getCursor()),\n\t\t\t-1,\n\t\t);\n\t}\n\n\tprivate registerClosePopoverListeners() {\n\t\tthis.getActiveView()?.containerEl.addEventListener(\n\t\t\t\"keydown\",\n\t\t\tthis.close,\n\t\t);\n\t\tthis.getActiveView()?.containerEl.addEventListener(\n\t\t\t\"click\",\n\t\t\tthis.clickClose,\n\t\t);\n\n\t\tif (this.mountedPopover) {\n\t\t\tthis.mountedPopover.addEventListener(\"mouseleave\", () => {\n\t\t\t\tconst popoverSettings = getSettings().defPopoverConfig;\n\t\t\t\tif (\n\t\t\t\t\tpopoverSettings.popoverDismissEvent ===\n\t\t\t\t\tPopoverDismissType.MouseExit\n\t\t\t\t) {\n\t\t\t\t\tthis.clickClose();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\tif (this.cmEditor) {\n\t\t\tthis.cmEditor.on(\"vim-keypress\", this.close);\n\t\t}\n\t\tconst scroller = this.getCmScroller();\n\t\tif (scroller) {\n\t\t\tscroller.addEventListener(\"scroll\", this.close);\n\t\t}\n\t}\n\n\tprivate unregisterClosePopoverListeners() {\n\t\tthis.getActiveView()?.containerEl.removeEventListener(\n\t\t\t\"keypress\",\n\t\t\tthis.close,\n\t\t);\n\t\tthis.getActiveView()?.containerEl.removeEventListener(\n\t\t\t\"click\",\n\t\t\tthis.clickClose,\n\t\t);\n\n\t\tif (this.cmEditor) {\n\t\t\tthis.cmEditor.off(\"vim-keypress\", this.close);\n\t\t}\n\t\tconst scroller = this.getCmScroller();\n\t\tif (scroller) {\n\t\t\tscroller.removeEventListener(\"scroll\", this.close);\n\t\t}\n\t}\n\n\tprivate getCmScroller() {\n\t\tconst scroller = document.getElementsByClassName(\"cm-scroller\");\n\t\tif (scroller.length > 0) {\n\t\t\treturn scroller[0];\n\t\t}\n\t}\n\n\tgetPopoverElement() {\n\t\treturn document.getElementById(\"definition-popover\");\n\t}\n\n\tprivate getActiveView() {\n\t\treturn this.app.workspace.getActiveViewOfType(MarkdownView);\n\t}\n}\n\n// Mount definition popover\nexport function initDefinitionPopover(plugin: Plugin) {\n\tif (definitionPopover) {\n\t\tdefinitionPopover.cleanUp();\n\t}\n\tdefinitionPopover = new DefinitionPopover(plugin);\n}\n\nexport function getDefinitionPopover() {\n\treturn definitionPopover;\n}\n"
  },
  {
    "path": "src/editor/definition-search.ts",
    "content": "import { getDefFileManager } from \"src/core/def-file-manager\";\nimport { PTreeNode, PTreeTraverser } from \"./prefix-tree\";\nimport { getSettings } from \"src/settings\";\n\n// Information of phrase that can be used to add decorations within the editor\nexport interface PhraseInfo {\n\tfrom: number;\n\tto: number;\n\tphrase: string;\n}\n\nexport class LineScanner {\n\tprefixTree: PTreeNode;\n\n\tprivate cnLangRegex = /\\p{Script=Han}/u;\n\tprivate terminatingCharRegex =\n\t\t/[!@#$%^&*()\\+={}[\\]:;\"'<>,.?\\/|\\\\\\r\\n （）＊＋，－／：；＜＝＞＠［＼］＾＿｀｛｜｝～｟｠｢｣､　、〃〈〉《》「」『』【】〔〕〖〗〘〙〚〛〜〝〞〟—‘’‛“”„‟…‧﹏﹑﹔·。]/;\n\n\tconstructor(pTree?: PTreeNode) {\n\t\tthis.prefixTree = pTree ? pTree : getDefFileManager().getPrefixTree();\n\t}\n\n\tscanLine(line: string, offset?: number): PhraseInfo[] {\n\t\tlet traversers: PTreeTraverser[] = [];\n\t\tconst phraseInfos: PhraseInfo[] = [];\n\n\t\tfor (let i = 0; i < line.length; i++) {\n\t\t\tlet c=\"\";\n\t\t\tif (getSettings().defFileParseConfig.enableCaseSensitive) {\n\t\t\t\tc = line.charAt(i);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tc = line.charAt(i).toLowerCase();\n\t\t\t}\n\t\t\tif (this.isValidStart(line, i)) {\n\t\t\t\ttraversers.push(new PTreeTraverser(this.prefixTree));\n\t\t\t}\n\n\t\t\ttraversers.forEach((traverser) => {\n\t\t\t\ttraverser.gotoNext(c);\n\t\t\t\tif (traverser.isWordEnd() && this.isValidEnd(line, i)) {\n\t\t\t\t\tconst phrase = traverser.getWord();\n\t\t\t\t\tphraseInfos.push({\n\t\t\t\t\t\tphrase: phrase,\n\t\t\t\t\t\tfrom: (offset ?? 0) + i - phrase.length + 1,\n\t\t\t\t\t\tto: (offset ?? 0) + i + 1,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Collect garbage traversers that hit a dead-end\n\t\t\ttraversers = traversers.filter((traverser) => {\n\t\t\t\treturn !!traverser.currPtr;\n\t\t\t});\n\t\t}\n\t\treturn phraseInfos;\n\t}\n\n\tprivate isValidEnd(line: string, ptr: number): boolean {\n\t\tlet c=\"\";\n\t\tif (getSettings().defFileParseConfig.enableCaseSensitive) {\n\t\t\tc = line.charAt(ptr);\n\t\t}\n\t\telse {\n\t\t\tc = line.charAt(ptr).toLowerCase();\n\t\t}\n\t\tif (this.isNonSpacedLanguage(c)) {\n\t\t\treturn true;\n\t\t}\n\t\t// If EOL, then it is a valid end\n\t\tif (ptr === line.length - 1) {\n\t\t\treturn true;\n\t\t}\n\t\t// Check if next character is a terminating character\n\t\treturn this.terminatingCharRegex.test(line.charAt(ptr + 1));\n\t}\n\n\t// Check if this character is a valid start of a word depending on the context\n\tprivate isValidStart(line: string, ptr: number): boolean {\n\t\tlet c=\"\";\n\t\tif (getSettings().defFileParseConfig.enableCaseSensitive) {\n\t\t\tc = line.charAt(ptr);\n\t\t}\n\t\telse {\n\t\t\tc = line.charAt(ptr).toLowerCase();\n\t\t}\n\t\tif (c == \" \") {\n\t\t\treturn false;\n\t\t}\n\t\tif (ptr === 0 || this.isNonSpacedLanguage(c)) {\n\t\t\treturn true;\n\t\t}\n\t\t// Check if previous character is a terminating character\n\t\treturn this.terminatingCharRegex.test(line.charAt(ptr - 1));\n\t}\n\n\tprivate isNonSpacedLanguage(c: string): boolean {\n\t\treturn this.cnLangRegex.test(c);\n\t}\n}\n"
  },
  {
    "path": "src/editor/edit-modal.ts",
    "content": "import { App, Modal } from \"obsidian\";\nimport { DefFileUpdater } from \"src/core/def-file-updater\";\nimport { Definition } from \"src/core/model\";\n\nexport class EditDefinitionModal {\n\tapp: App;\n\tmodal: Modal;\n\taliases: string;\n\tdefinition: string;\n\tsubmitting: boolean;\n\n\tconstructor(app: App) {\n\t\tthis.app = app;\n\t\tthis.modal = new Modal(app);\n\t}\n\n\topen(def: Definition) {\n\t\tthis.submitting = false;\n\t\tthis.modal.setTitle(`Edit definition for '${def.word}'`);\n\t\tthis.modal.contentEl.createDiv({\n\t\t\tcls: \"edit-modal-section-header\",\n\t\t\ttext: \"Aliases\",\n\t\t});\n\t\tconst aliasText = this.modal.contentEl.createEl(\"textarea\", {\n\t\t\tcls: \"edit-modal-aliases\",\n\t\t\tattr: {\n\t\t\t\tplaceholder: \"Add comma-separated aliases here\",\n\t\t\t},\n\t\t\ttext: def.aliases.join(\", \"),\n\t\t});\n\t\tthis.modal.contentEl.createDiv({\n\t\t\tcls: \"edit-modal-section-header\",\n\t\t\ttext: \"Definition\",\n\t\t});\n\t\tconst defText = this.modal.contentEl.createEl(\"textarea\", {\n\t\t\tcls: \"edit-modal-textarea\",\n\t\t\tattr: {\n\t\t\t\tplaceholder: \"Add definition here\",\n\t\t\t},\n\t\t\ttext: def.definition,\n\t\t});\n\t\tconst button = this.modal.contentEl.createEl(\"button\", {\n\t\t\ttext: \"Save\",\n\t\t\tcls: \"edit-modal-save-button\",\n\t\t});\n\t\tbutton.addEventListener(\"click\", () => {\n\t\t\tthis.submit(def, aliasText, defText);\n\t\t});\n\n\t\t// set up key event listeners for closing and submitting the modal\n\t\tthis.modal.scope.register([\"Mod\"], \"Enter\", () => {\n\t\t\tthis.submit(def, defText, aliasText);\n\t\t});\n\n\t\tthis.modal.open();\n\t}\n\n\tsubmit(\n\t\tdef: Definition,\n\t\taliasText: HTMLTextAreaElement,\n\t\tdefText: HTMLTextAreaElement,\n\t) {\n\t\tif (this.submitting) {\n\t\t\treturn;\n\t\t}\n\t\tconst updater = new DefFileUpdater(this.app);\n\t\tupdater.updateDefinition({\n\t\t\t...def,\n\t\t\taliases: aliasText.value\n\t\t\t\t? aliasText.value.split(\",\").map((alias) => alias.trim())\n\t\t\t\t: [],\n\t\t\tdefinition: defText.value,\n\t\t});\n\t\tthis.modal.close();\n\t}\n}\n"
  },
  {
    "path": "src/editor/frontmatter-suggest-modal.ts",
    "content": "import {\n\tApp,\n\tFuzzySuggestModal,\n\tNotice,\n\tTAbstractFile,\n\tTFile,\n\tTFolder,\n} from \"obsidian\";\nimport { DEF_CTX_FM_KEY, getDefFileManager } from \"src/core/def-file-manager\";\nimport { logError } from \"src/util/log\";\n\nexport class FMSuggestModal extends FuzzySuggestModal<TAbstractFile> {\n\tfile: TFile;\n\n\tconstructor(app: App, currFile: TFile) {\n\t\tsuper(app);\n\t\tthis.file = currFile;\n\t}\n\n\tgetItems(): TAbstractFile[] {\n\t\tconst defManager = getDefFileManager();\n\t\treturn [...defManager.getDefFiles(), ...defManager.getDefFolders()];\n\t}\n\n\tgetItemText(item: TAbstractFile): string {\n\t\treturn this.getPath(item);\n\t}\n\n\tonChooseItem(item: TAbstractFile, evt: MouseEvent | KeyboardEvent) {\n\t\tconst path = this.getPath(item);\n\t\tthis.app.fileManager\n\t\t\t.processFrontMatter(this.file, (fm) => {\n\t\t\t\tlet currDefSource = fm[DEF_CTX_FM_KEY];\n\n\t\t\t\tif (!currDefSource || !Array.isArray(currDefSource)) {\n\t\t\t\t\tcurrDefSource = [];\n\t\t\t\t} else if (currDefSource.includes(path)) {\n\t\t\t\t\tnew Notice(\n\t\t\t\t\t\t\"Definition file source is already included for this file\",\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tfm[DEF_CTX_FM_KEY] = [...currDefSource, path];\n\t\t\t})\n\t\t\t.catch((e) => {\n\t\t\t\tlogError(`Error writing to frontmatter of file: ${e}`);\n\t\t\t});\n\t}\n\n\tprivate getPath(file: TAbstractFile): string {\n\t\tif (file instanceof TFolder) {\n\t\t\treturn file.path + \"/\";\n\t\t}\n\t\treturn file.path;\n\t}\n}\n"
  },
  {
    "path": "src/editor/md-postprocessor.ts",
    "content": "import { MarkdownPostProcessor } from \"obsidian\";\nimport { getDefFileManager } from \"src/core/def-file-manager\";\nimport { getSettings } from \"src/settings\";\nimport { DEF_DECORATION_CLS, getDecorationAttrs } from \"./common\";\nimport { getDefinitionPopover } from \"./definition-popover\";\nimport { LineScanner, PhraseInfo } from \"./definition-search\";\n\nconst DEF_LINK_DECOR_CLS = \"def-link-decoration\";\n\ninterface Marks {\n\tel: HTMLElement;\n\tphraseInfo: PhraseInfo;\n}\n\nexport const postProcessor: MarkdownPostProcessor = (element, context) => {\n\tconst shouldRunPostProcessor =\n\t\twindow.NoteDefinition.settings.enableInReadingView;\n\tif (!shouldRunPostProcessor) {\n\t\treturn;\n\t}\n\n\tconst popoverSettings = getSettings().defPopoverConfig;\n\n\t// Prevent post-processing for definition popover\n\tconst isPopupCtx = element.getAttr(\"ctx\") === \"def-popup\";\n\tif (isPopupCtx && !popoverSettings.enableDefinitionLink) {\n\t\treturn;\n\t}\n\n\trebuildHTML(element, isPopupCtx);\n};\n\nconst rebuildHTML = (parent: Node, isPopupCtx: boolean) => {\n\t// Skip the function entirely (including recursion into node's children) to disable formatting on links\n\tif (!getSettings().enableOnLinks && parent.nodeName === \"A\") {\n\t\treturn;\n\t}\n\n\tfor (let i = 0; i < parent.childNodes.length; i++) {\n\t\tconst childNode = parent.childNodes[i];\n\t\t// Replace only if TEXT_NODE\n\t\tif (childNode.nodeType === Node.TEXT_NODE && childNode.textContent) {\n\t\t\tif (childNode.textContent === \"\\n\") {\n\t\t\t\t// Ignore nodes with just a newline char\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst lineScanner = new LineScanner();\n\t\t\tconst currText = childNode.textContent;\n\t\t\tconst phraseInfos = lineScanner.scanLine(currText);\n\t\t\tif (phraseInfos.length === 0) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Decorations need to be sorted by 'from' ascending, then 'to' descending\n\t\t\t// This allows us to prefer longer words over shorter ones\n\t\t\tphraseInfos.sort((a, b) => b.to - a.to);\n\t\t\tphraseInfos.sort((a, b) => a.from - b.from);\n\n\t\t\tlet currCursor = 0;\n\t\t\tconst newContainer = parent.createSpan();\n\t\t\tconst addedMarks: Marks[] = [];\n\n\t\t\tconst popoverSettings = getSettings().defPopoverConfig;\n\n\t\t\tphraseInfos.forEach((phraseInfo) => {\n\t\t\t\tif (phraseInfo.from < currCursor) {\n\t\t\t\t\t// Subset or intersect phrases are ignored\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tnewContainer.appendText(\n\t\t\t\t\tcurrText.slice(currCursor, phraseInfo.from),\n\t\t\t\t);\n\n\t\t\t\tlet span: HTMLSpanElement;\n\t\t\t\tif (isPopupCtx && popoverSettings.enableDefinitionLink) {\n\t\t\t\t\tspan = getLinkDecorationSpan(\n\t\t\t\t\t\tnewContainer,\n\t\t\t\t\t\tphraseInfo,\n\t\t\t\t\t\tcurrText,\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\tspan = getNormalDecorationSpan(\n\t\t\t\t\t\tnewContainer,\n\t\t\t\t\t\tphraseInfo,\n\t\t\t\t\t\tcurrText,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tnewContainer.appendChild(span);\n\t\t\t\taddedMarks.push({\n\t\t\t\t\tel: span,\n\t\t\t\t\tphraseInfo: phraseInfo,\n\t\t\t\t});\n\t\t\t\tcurrCursor = phraseInfo.to;\n\t\t\t});\n\n\t\t\tnewContainer.appendText(currText.slice(currCursor));\n\t\t\tchildNode.replaceWith(newContainer);\n\t\t}\n\n\t\trebuildHTML(childNode, isPopupCtx);\n\t}\n};\n\nfunction getNormalDecorationSpan(\n\tcontainer: HTMLElement,\n\tphraseInfo: PhraseInfo,\n\tcurrText: string,\n): HTMLSpanElement {\n\tconst attributes = getDecorationAttrs(phraseInfo.phrase);\n\tconst span = container.createSpan({\n\t\tcls: DEF_DECORATION_CLS,\n\t\tattr: attributes,\n\t\ttext: currText.slice(phraseInfo.from, phraseInfo.to),\n\t});\n\treturn span;\n}\n\nfunction getLinkDecorationSpan(\n\tcontainer: HTMLElement,\n\tphraseInfo: PhraseInfo,\n\tcurrText: string,\n): HTMLSpanElement {\n\tconst span = container.createSpan({\n\t\tcls: DEF_LINK_DECOR_CLS,\n\t\ttext: currText.slice(phraseInfo.from, phraseInfo.to),\n\t});\n\tspan.addEventListener(\"click\", (e) => {\n\t\tconst app = window.NoteDefinition.app;\n\t\tconst def = getDefFileManager().get(phraseInfo.phrase);\n\t\tif (!def) {\n\t\t\treturn;\n\t\t}\n\t\tapp.workspace.openLinkText(def.linkText, \"\");\n\t\t// Close definition popover\n\t\tconst popover = getDefinitionPopover();\n\t\tif (popover) {\n\t\t\tpopover.close();\n\t\t}\n\t});\n\treturn span;\n}\n"
  },
  {
    "path": "src/editor/mobile/definition-modal.ts",
    "content": "import {\n\tApp,\n\tComponent,\n\tMarkdownRenderer,\n\tnormalizePath,\n\tModal,\n} from \"obsidian\";\nimport { Definition } from \"src/core/model\";\n\nlet defModal: DefinitionModal;\n\nexport class DefinitionModal extends Component {\n\tapp: App;\n\tmodal: Modal;\n\n\tconstructor(app: App) {\n\t\tsuper();\n\t\tthis.app = app;\n\t\tthis.modal = new Modal(app);\n\t}\n\n\topen(definition: Definition) {\n\t\tthis.modal.contentEl.empty();\n\t\tthis.modal.contentEl.createEl(\"h1\", {\n\t\t\ttext: definition.word,\n\t\t});\n\t\tthis.modal.contentEl.createEl(\"i\", {\n\t\t\ttext: definition.aliases.join(\", \"),\n\t\t});\n\t\tconst defContent = this.modal.contentEl.createEl(\"div\", {\n\t\t\tattr: {\n\t\t\t\tctx: \"def-popup\",\n\t\t\t},\n\t\t});\n\t\tMarkdownRenderer.render(\n\t\t\tthis.app,\n\t\t\tdefinition.definition,\n\t\t\tdefContent,\n\t\t\tnormalizePath(definition.file.path) ?? \"\",\n\t\t\tthis,\n\t\t);\n\t\tthis.modal.open();\n\t}\n}\n\nexport function initDefinitionModal(app: App) {\n\tdefModal = new DefinitionModal(app);\n\treturn defModal;\n}\n\nexport function getDefinitionModal() {\n\treturn defModal;\n}\n"
  },
  {
    "path": "src/editor/prefix-tree.ts",
    "content": "// Prefix tree node\nexport class PTreeNode {\n\tchildren: Map<string, PTreeNode>;\n\twordEnd: boolean;\n\n\tconstructor() {\n\t\tthis.children = new Map<string, PTreeNode>();\n\t\tthis.wordEnd = false;\n\t}\n\n\tadd(word: string, ptr?: number) {\n\t\tif (ptr === undefined) {\n\t\t\tptr = 0;\n\t\t}\n\t\tif (ptr === word.length) {\n\t\t\tthis.wordEnd = true;\n\t\t\treturn;\n\t\t}\n\t\tconst currChar = word.charAt(ptr);\n\t\tlet nextNode;\n\t\tnextNode = this.children.get(currChar);\n\t\tif (!nextNode) {\n\t\t\tnextNode = new PTreeNode();\n\t\t\tthis.children.set(currChar, nextNode);\n\t\t}\n\t\tnextNode.add(word, ++ptr);\n\t}\n}\n\n// A traverser implementation to traverse the prefix tree and keep track of states\nexport class PTreeTraverser {\n\tcurrPtr?: PTreeNode;\n\twordBuf: Array<string>;\n\n\tconstructor(root: PTreeNode) {\n\t\tthis.currPtr = root;\n\t\tthis.wordBuf = [];\n\t}\n\n\tgotoNext(c: string) {\n\t\tif (!this.currPtr) {\n\t\t\treturn;\n\t\t}\n\t\tconst nextNode = this.currPtr.children.get(c);\n\t\t// This will set currPtr to undefined if there is no next node\n\t\t// This marks the traverser as garbage to be collected\n\t\tthis.currPtr = nextNode;\n\t\tthis.wordBuf.push(c);\n\t}\n\n\tisWordEnd() {\n\t\tif (!this.currPtr) {\n\t\t\treturn false;\n\t\t}\n\t\treturn this.currPtr.wordEnd;\n\t}\n\n\tgetWord() {\n\t\treturn this.wordBuf.join(\"\");\n\t}\n}\n"
  },
  {
    "path": "src/globals.ts",
    "content": "import { App, Platform } from \"obsidian\";\nimport { DefinitionRepo, getDefFileManager } from \"./core/def-file-manager\";\nimport { getDefinitionPopover } from \"./editor/definition-popover\";\nimport { getDefinitionModal } from \"./editor/mobile/definition-modal\";\nimport { getSettings, PopoverDismissType, Settings } from \"./settings\";\nimport { LogLevel } from \"./util/log\";\n\nexport {};\n\ndeclare global {\n\tinterface Window {\n\t\tNoteDefinition: GlobalVars;\n\t}\n}\n\nexport interface GlobalVars {\n\tLOG_LEVEL: LogLevel;\n\tdefinitions: {\n\t\tglobal: DefinitionRepo;\n\t};\n\ttriggerDefPreview: (el: HTMLElement) => void;\n\tsettings: Settings;\n\tapp: App;\n}\n\n// Initialise and inject globals\nexport function injectGlobals(\n\tsettings: Settings,\n\tapp: App,\n\ttargetWindow: Window,\n) {\n\ttargetWindow.NoteDefinition = {\n\t\tapp: app,\n\t\tLOG_LEVEL: activeWindow.NoteDefinition?.LOG_LEVEL || LogLevel.Error,\n\t\tdefinitions: {\n\t\t\tglobal: new DefinitionRepo(),\n\t\t},\n\t\ttriggerDefPreview: (el: HTMLElement) => {\n\t\t\tconst word = el.getAttr(\"def\");\n\t\t\tif (!word) return;\n\n\t\t\tconst def = getDefFileManager().get(word);\n\t\t\tif (!def) return;\n\n\t\t\tif (Platform.isMobile) {\n\t\t\t\tconst defModal = getDefinitionModal();\n\t\t\t\tdefModal.open(def);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst defPopover = getDefinitionPopover();\n\t\t\tlet isOpen = false;\n\n\t\t\tif (el.onmouseenter) {\n\t\t\t\tconst openPopover = setTimeout(() => {\n\t\t\t\t\tdefPopover.openAtCoords(def, el.getBoundingClientRect());\n\t\t\t\t\tisOpen = true;\n\t\t\t\t}, 200);\n\n\t\t\t\tel.onmouseleave = () => {\n\t\t\t\t\tconst popoverSettings = getSettings().defPopoverConfig;\n\t\t\t\t\tif (!isOpen) {\n\t\t\t\t\t\tclearTimeout(openPopover);\n\t\t\t\t\t} else if (\n\t\t\t\t\t\tpopoverSettings.popoverDismissEvent ===\n\t\t\t\t\t\tPopoverDismissType.MouseExit\n\t\t\t\t\t) {\n\t\t\t\t\t\tdefPopover.clickClose();\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tdefPopover.openAtCoords(def, el.getBoundingClientRect());\n\t\t},\n\t\tsettings,\n\t};\n}\n"
  },
  {
    "path": "src/main.ts",
    "content": "import {\n\tMenu,\n\tNotice,\n\tPlugin,\n\tTFolder,\n\tWorkspaceWindow,\n\tTFile,\n\tMarkdownView,\n} from \"obsidian\";\nimport { injectGlobals } from \"./globals\";\nimport { logDebug } from \"./util/log\";\nimport { definitionMarker } from \"./editor/decoration\";\nimport { Extension } from \"@codemirror/state\";\nimport { EditorView } from \"@codemirror/view\";\nimport { DefManager, initDefFileManager } from \"./core/def-file-manager\";\nimport { Definition } from \"./core/model\";\nimport {\n\tgetDefinitionPopover,\n\tinitDefinitionPopover,\n} from \"./editor/definition-popover\";\nimport { postProcessor } from \"./editor/md-postprocessor\";\nimport { DEFAULT_SETTINGS, getSettings, SettingsTab } from \"./settings\";\nimport { getMarkedWordUnderCursor } from \"./util/editor\";\nimport {\n\tFileExplorerDecoration,\n\tinitFileExplorerDecoration,\n} from \"./ui/file-explorer\";\nimport { EditDefinitionModal } from \"./editor/edit-modal\";\nimport { AddDefinitionModal } from \"./editor/add-modal\";\nimport { initDefinitionModal } from \"./editor/mobile/definition-modal\";\nimport { FMSuggestModal } from \"./editor/frontmatter-suggest-modal\";\nimport { registerDefFile } from \"./editor/def-file-registration\";\nimport { DefFileType } from \"./core/file-type\";\n\nexport default class NoteDefinition extends Plugin {\n\tactiveEditorExtensions: Extension[] = [];\n\tdefManager: DefManager;\n\tfileExplorerDeco: FileExplorerDecoration;\n\n\tasync onload() {\n\t\t// Settings are injected into global object\n\t\tconst settings = Object.assign(\n\t\t\t{},\n\t\t\tDEFAULT_SETTINGS,\n\t\t\tawait this.loadData(),\n\t\t);\n\t\tinjectGlobals(settings, this.app, window);\n\n\t\tthis.registerEvent(\n\t\t\tthis.app.workspace.on(\n\t\t\t\t\"window-open\",\n\t\t\t\t(win: WorkspaceWindow, newWindow: Window) => {\n\t\t\t\t\tinjectGlobals(settings, this.app, newWindow);\n\t\t\t\t},\n\t\t\t),\n\t\t);\n\n\t\tlogDebug(\"Load note definition plugin\");\n\n\t\tinitDefinitionPopover(this);\n\t\tinitDefinitionModal(this.app);\n\t\tthis.defManager = initDefFileManager(this.app);\n\t\tthis.fileExplorerDeco = initFileExplorerDecoration(this.app);\n\t\tthis.registerEditorExtension(this.activeEditorExtensions);\n\t\tthis.updateEditorExts();\n\n\t\tthis.registerCommands();\n\t\tthis.registerEvents();\n\n\t\tthis.addSettingTab(\n\t\t\tnew SettingsTab(this.app, this, this.saveSettings.bind(this)),\n\t\t);\n\t\tthis.registerMarkdownPostProcessor(postProcessor);\n\n\t\tthis.fileExplorerDeco.run();\n\t}\n\n\tasync saveSettings() {\n\t\tawait this.saveData(window.NoteDefinition.settings);\n\t\tthis.fileExplorerDeco.run();\n\t\tthis.refreshDefinitions();\n\t}\n\n\tregisterCommands() {\n\t\tthis.addCommand({\n\t\t\tid: \"preview-definition\",\n\t\t\tname: \"Preview definition\",\n\t\t\teditorCallback: (editor) => {\n\t\t\t\tconst curWord = getMarkedWordUnderCursor(editor);\n\t\t\t\tif (!curWord) return;\n\t\t\t\tconst def =\n\t\t\t\t\twindow.NoteDefinition.definitions.global.get(curWord);\n\t\t\t\tif (!def) return;\n\t\t\t\tgetDefinitionPopover().openAtCursor(def);\n\t\t\t},\n\t\t});\n\n\t\tthis.addCommand({\n\t\t\tid: \"goto-definition\",\n\t\t\tname: \"Go to definition\",\n\t\t\teditorCallback: (editor) => {\n\t\t\t\tconst currWord = getMarkedWordUnderCursor(editor);\n\t\t\t\tif (!currWord) return;\n\t\t\t\tconst def = this.defManager.get(currWord);\n\t\t\t\tif (!def) return;\n\t\t\t\tthis.app.workspace.openLinkText(def.linkText, \"\");\n\t\t\t},\n\t\t});\n\n\t\tthis.addCommand({\n\t\t\tid: \"add-definition\",\n\t\t\tname: \"Add definition\",\n\t\t\teditorCallback: (editor) => {\n\t\t\t\tconst selectedText = editor.getSelection();\n\t\t\t\tconst addModal = new AddDefinitionModal(this.app);\n\t\t\t\taddModal.open(selectedText);\n\t\t\t},\n\t\t});\n\n\t\tthis.addCommand({\n\t\t\tid: \"add-def-context\",\n\t\t\tname: \"Add definition context\",\n\t\t\teditorCallback: (editor) => {\n\t\t\t\tconst activeFile = this.app.workspace.getActiveFile();\n\t\t\t\tif (!activeFile) {\n\t\t\t\t\tnew Notice(\n\t\t\t\t\t\t\"Command must be used within an active opened file\",\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst suggestModal = new FMSuggestModal(this.app, activeFile);\n\t\t\t\tsuggestModal.open();\n\t\t\t},\n\t\t});\n\n\t\tthis.addCommand({\n\t\t\tid: \"refresh-definitions\",\n\t\t\tname: \"Refresh definitions\",\n\t\t\tcallback: () => {\n\t\t\t\tthis.fileExplorerDeco.run();\n\t\t\t\tthis.defManager.loadDefinitions();\n\t\t\t},\n\t\t});\n\n\t\tthis.addCommand({\n\t\t\tid: \"register-consolidated-def-file\",\n\t\t\tname: \"Register consolidated definition file\",\n\t\t\teditorCallback: (_) => {\n\t\t\t\tconst activeFile = this.app.workspace.getActiveFile();\n\t\t\t\tif (!activeFile) {\n\t\t\t\t\tnew Notice(\n\t\t\t\t\t\t\"Command must be used within an active opened file\",\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tregisterDefFile(this.app, activeFile, DefFileType.Consolidated);\n\t\t\t},\n\t\t});\n\n\t\tthis.addCommand({\n\t\t\tid: \"register-atomic-def-file\",\n\t\t\tname: \"Register atomic definition file\",\n\t\t\teditorCallback: (_) => {\n\t\t\t\tconst activeFile = this.app.workspace.getActiveFile();\n\t\t\t\tif (!activeFile) {\n\t\t\t\t\tnew Notice(\n\t\t\t\t\t\t\"Command must be used within an active opened file\",\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tregisterDefFile(this.app, activeFile, DefFileType.Atomic);\n\t\t\t},\n\t\t});\n\t}\n\n\tregisterEvents() {\n\t\tthis.registerEvent(\n\t\t\tthis.app.workspace.on(\"active-leaf-change\", async (leaf) => {\n\t\t\t\tif (!leaf) return;\n\t\t\t\tthis.reloadUpdatedDefinitions();\n\t\t\t\tthis.updateEditorExts();\n\t\t\t\tthis.defManager.updateActiveFile();\n\t\t\t}),\n\t\t);\n\n\t\tthis.registerEvent(\n\t\t\tthis.app.workspace.on(\"editor-menu\", (menu, editor) => {\n\t\t\t\tconst defPopover = getDefinitionPopover();\n\t\t\t\tif (defPopover) {\n\t\t\t\t\tdefPopover.close();\n\t\t\t\t}\n\n\t\t\t\tconst curWord = getMarkedWordUnderCursor(editor);\n\t\t\t\tif (!curWord) {\n\t\t\t\t\tif (editor.getSelection()) {\n\t\t\t\t\t\tmenu.addItem((item) => {\n\t\t\t\t\t\t\titem.setTitle(\"Add definition\");\n\t\t\t\t\t\t\titem.setIcon(\"plus\").onClick(() => {\n\t\t\t\t\t\t\t\tconst addModal = new AddDefinitionModal(\n\t\t\t\t\t\t\t\t\tthis.app,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\taddModal.open(editor.getSelection());\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst def = this.defManager.get(curWord);\n\t\t\t\tif (!def) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.registerMenuForMarkedWords(menu, def);\n\t\t\t}),\n\t\t);\n\n\t\t// Add file menu options\n\t\tthis.registerEvent(\n\t\t\tthis.app.workspace.on(\"file-menu\", (menu, file, source) => {\n\t\t\t\tif (file instanceof TFolder) {\n\t\t\t\t\tmenu.addItem((item) => {\n\t\t\t\t\t\titem.setTitle(\"Set definition folder\")\n\t\t\t\t\t\t\t.setIcon(\"book-a\")\n\t\t\t\t\t\t\t.onClick(() => {\n\t\t\t\t\t\t\t\tconst settings = getSettings();\n\t\t\t\t\t\t\t\tsettings.defFolder = file.path;\n\t\t\t\t\t\t\t\tthis.saveSettings();\n\t\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\n\t\t// Creating files under def folder should register file as definition file\n\t\tthis.registerEvent(\n\t\t\tthis.app.vault.on(\"create\", (file) => {\n\t\t\t\tconst settings = getSettings();\n\t\t\t\tif (file.path.startsWith(settings.defFolder)) {\n\t\t\t\t\tthis.fileExplorerDeco.run();\n\t\t\t\t\tthis.refreshDefinitions();\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\n\t\tthis.registerEvent(\n\t\t\tthis.app.metadataCache.on(\"changed\", (file: TFile) => {\n\t\t\t\tconst currFile = this.app.workspace.getActiveFile();\n\n\t\t\t\tif (currFile && currFile.path === file.path) {\n\t\t\t\t\tthis.defManager.updateActiveFile();\n\n\t\t\t\t\tlet activeView =\n\t\t\t\t\t\tthis.app.workspace.getActiveViewOfType(MarkdownView);\n\n\t\t\t\t\tif (activeView) {\n\t\t\t\t\t\t// @ts-expect-error, not typed\n\t\t\t\t\t\tconst view = activeView.editor.cm as EditorView;\n\t\t\t\t\t\tconst plugin = view.plugin(definitionMarker);\n\n\t\t\t\t\t\tif (plugin) {\n\t\t\t\t\t\t\tplugin.forceUpdate();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\t}\n\n\tregisterMenuForMarkedWords(menu: Menu, def: Definition) {\n\t\tmenu.addItem((item) => {\n\t\t\titem.setTitle(\"Go to definition\")\n\t\t\t\t.setIcon(\"arrow-left-from-line\")\n\t\t\t\t.onClick(() => {\n\t\t\t\t\tthis.app.workspace.openLinkText(def.linkText, \"\");\n\t\t\t\t});\n\t\t});\n\n\t\tmenu.addItem((item) => {\n\t\t\titem.setTitle(\"Edit definition\")\n\t\t\t\t.setIcon(\"pencil\")\n\t\t\t\t.onClick(() => {\n\t\t\t\t\tconst editModal = new EditDefinitionModal(this.app);\n\t\t\t\t\teditModal.open(def);\n\t\t\t\t});\n\t\t});\n\t}\n\n\trefreshDefinitions() {\n\t\tthis.defManager.loadDefinitions();\n\t}\n\n\treloadUpdatedDefinitions() {\n\t\tthis.defManager.loadUpdatedFiles();\n\t}\n\n\tupdateEditorExts() {\n\t\tconst currFile = this.app.workspace.getActiveFile();\n\t\tif (currFile && this.defManager.isDefFile(currFile)) {\n\t\t\t// TODO: Editor extension for definition file\n\t\t\tthis.setActiveEditorExtensions([]);\n\t\t} else {\n\t\t\tthis.setActiveEditorExtensions(definitionMarker);\n\t\t}\n\t}\n\n\tprivate setActiveEditorExtensions(...ext: Extension[]) {\n\t\tthis.activeEditorExtensions.length = 0;\n\t\tthis.activeEditorExtensions.push(...ext);\n\t\tthis.app.workspace.updateOptions();\n\t}\n\n\tonunload() {\n\t\tlogDebug(\"Unload note definition plugin\");\n\t\tgetDefinitionPopover().cleanUp();\n\t}\n}\n"
  },
  {
    "path": "src/settings.ts",
    "content": "import {\n\tApp,\n\tModal,\n\tNotice,\n\tPlugin,\n\tPluginSettingTab,\n\tSetting,\n\tsetTooltip,\n} from \"obsidian\";\nimport { DefFileType } from \"./core/file-type\";\n\nexport enum PopoverEventSettings {\n\tHover = \"hover\",\n\tClick = \"click\",\n}\n\nexport enum PopoverDismissType {\n\tClick = \"click\",\n\tMouseExit = \"mouse_exit\",\n}\n\nexport interface DividerSettings {\n\tdash: boolean;\n\tunderscore: boolean;\n}\n\nexport interface DefFileParseConfig {\n\tdefaultFileType: DefFileType;\n\tdivider: DividerSettings;\n\tautoPlurals: boolean;\n\tenableCaseSensitive: boolean;\n}\n\nexport interface DefinitionPopoverConfig {\n\tdisplayAliases: boolean;\n\tdisplayDefFileName: boolean;\n\tenableCustomSize: boolean;\n\tmaxWidth: number;\n\tmaxHeight: number;\n\tpopoverDismissEvent: PopoverDismissType;\n\tenableDefinitionLink: boolean;\n\tbackgroundColour?: string;\n}\n\nexport interface Settings {\n\tenableInReadingView: boolean;\n\tenableOnLinks: boolean;\n\tenableSpellcheck: boolean;\n\tdefFolder: string;\n\tpopoverEvent: PopoverEventSettings;\n\tdefFileParseConfig: DefFileParseConfig;\n\tdefPopoverConfig: DefinitionPopoverConfig;\n}\n\nexport const VALID_DEFINITION_FILE_TYPES = [\".md\"];\n\nexport const DEFAULT_DEF_FOLDER = \"definitions\";\n\nexport const DEFAULT_SETTINGS: Partial<Settings> = {\n\tenableInReadingView: true,\n\tenableOnLinks: true,\n\tenableSpellcheck: true,\n\tpopoverEvent: PopoverEventSettings.Hover,\n\tdefFileParseConfig: {\n\t\tdefaultFileType: DefFileType.Consolidated,\n\t\tdivider: {\n\t\t\tdash: true,\n\t\t\tunderscore: false,\n\t\t},\n\t\tautoPlurals: false,\n\t\tenableCaseSensitive: false,\n\t},\n\tdefPopoverConfig: {\n\t\tdisplayAliases: true,\n\t\tdisplayDefFileName: false,\n\t\tenableCustomSize: false,\n\t\tmaxWidth: 150,\n\t\tmaxHeight: 150,\n\t\tpopoverDismissEvent: PopoverDismissType.Click,\n\t\tenableDefinitionLink: false,\n\t},\n};\n\nexport class SettingsTab extends PluginSettingTab {\n\tplugin: Plugin;\n\tsettings: Settings;\n\tsaveCallback: () => Promise<void>;\n\n\tconstructor(app: App, plugin: Plugin, saveCallback: () => Promise<void>) {\n\t\tsuper(app, plugin);\n\t\tthis.plugin = plugin;\n\t\tthis.settings = window.NoteDefinition.settings;\n\t\tthis.saveCallback = saveCallback;\n\t}\n\n\tdisplay(): void {\n\t\tlet { containerEl } = this;\n\n\t\tcontainerEl.empty();\n\n\t\tnew Setting(containerEl)\n\t\t\t.setName(\"Enable in Reading View\")\n\t\t\t.setDesc(\n\t\t\t\t\"Allow defined phrases and definition popovers to be shown in Reading View\",\n\t\t\t)\n\t\t\t.addToggle((component) => {\n\t\t\t\tcomponent.setValue(this.settings.enableInReadingView);\n\t\t\t\tcomponent.onChange(async (val) => {\n\t\t\t\t\tthis.settings.enableInReadingView = val;\n\t\t\t\t\tawait this.saveCallback();\n\t\t\t\t\tthis.display();\n\t\t\t\t});\n\t\t\t});\n\n\t\tif (this.settings.enableInReadingView) {\n\t\t\tnew Setting(containerEl)\n\t\t\t\t.setName(\"Enable highlight on links\")\n\t\t\t\t.setDesc(\n\t\t\t\t\t\"Allow defined phrases and definition popovers to display on links (only applies to Reading View)\",\n\t\t\t\t)\n\t\t\t\t.addToggle((component) => {\n\t\t\t\t\tcomponent.setValue(this.settings.enableOnLinks);\n\t\t\t\t\tcomponent.onChange(async (val) => {\n\t\t\t\t\t\tthis.settings.enableOnLinks = val;\n\t\t\t\t\t\tawait this.saveCallback();\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t}\n\n\t\tnew Setting(containerEl)\n\t\t\t.setName(\"Enable spellcheck for defined words\")\n\t\t\t.setDesc(\"Allow defined words and phrases to be spellchecked\")\n\t\t\t.addToggle((component) => {\n\t\t\t\tcomponent.setValue(this.settings.enableSpellcheck);\n\t\t\t\tcomponent.onChange(async (val) => {\n\t\t\t\t\tthis.settings.enableSpellcheck = val;\n\t\t\t\t\tawait this.saveCallback();\n\t\t\t\t});\n\t\t\t});\n\n\t\tnew Setting(containerEl)\n\t\t\t.setName(\"Enable Case Sensitivity\")\n\t\t\t.setDesc(\"Only match if the cases of both terms match\")\n\t\t\t.addToggle((component) => {\n\t\t\t\tcomponent.setValue(this.settings.defFileParseConfig.enableCaseSensitive);\n\t\t\t\tcomponent.onChange(async (val) => {\n\t\t\t\t\tthis.settings.defFileParseConfig.enableCaseSensitive = val;\n\t\t\t\t\tawait this.saveCallback();\n\t\t\t\t});\n\t\t\t});\n\n\t\tnew Setting(containerEl)\n\t\t\t.setName(\"Definitions folder\")\n\t\t\t.setDesc(\n\t\t\t\t\"Files within this folder will be parsed to register definitions\",\n\t\t\t)\n\t\t\t.addText((component) => {\n\t\t\t\tcomponent.setValue(this.settings.defFolder);\n\t\t\t\tcomponent.setPlaceholder(DEFAULT_DEF_FOLDER);\n\t\t\t\tcomponent.setDisabled(true);\n\t\t\t\tsetTooltip(\n\t\t\t\t\tcomponent.inputEl,\n\t\t\t\t\t\"In the file explorer, right-click on the desired folder and click on 'Set definition folder' to change the definition folder\",\n\t\t\t\t\t{\n\t\t\t\t\t\tdelay: 100,\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t});\n\n\t\tnew Setting(containerEl)\n\t\t\t.setName(\"Definition file format settings\")\n\t\t\t.setDesc(\"Customise parsing rules for definition files\")\n\t\t\t.addExtraButton((component) => {\n\t\t\t\tcomponent.onClick(() => {\n\t\t\t\t\tconst modal = new Modal(this.app);\n\t\t\t\t\tmodal.setTitle(\"Definition file format settings\");\n\t\t\t\t\tnew Setting(modal.contentEl)\n\t\t\t\t\t\t.setName(\"Divider\")\n\t\t\t\t\t\t.setHeading();\n\t\t\t\t\tnew Setting(modal.contentEl)\n\t\t\t\t\t\t.setName(\"Dash\")\n\t\t\t\t\t\t.setDesc(\"Use triple dash (---) as divider\")\n\t\t\t\t\t\t.addToggle((component) => {\n\t\t\t\t\t\t\tcomponent.setValue(\n\t\t\t\t\t\t\t\tthis.settings.defFileParseConfig.divider.dash,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tcomponent.onChange(async (value) => {\n\t\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t\t!value &&\n\t\t\t\t\t\t\t\t\t!this.settings.defFileParseConfig.divider\n\t\t\t\t\t\t\t\t\t\t.underscore\n\t\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\t\tnew Notice(\n\t\t\t\t\t\t\t\t\t\t\"At least one divider must be chosen\",\n\t\t\t\t\t\t\t\t\t\t2000,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\tcomponent.setValue(\n\t\t\t\t\t\t\t\t\t\tthis.settings.defFileParseConfig.divider\n\t\t\t\t\t\t\t\t\t\t\t.dash,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tthis.settings.defFileParseConfig.divider.dash =\n\t\t\t\t\t\t\t\t\tvalue;\n\t\t\t\t\t\t\t\tawait this.saveCallback();\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t});\n\t\t\t\t\tnew Setting(modal.contentEl)\n\t\t\t\t\t\t.setName(\"Underscore\")\n\t\t\t\t\t\t.setDesc(\"Use triple underscore (___) as divider\")\n\t\t\t\t\t\t.addToggle((component) => {\n\t\t\t\t\t\t\tcomponent.setValue(\n\t\t\t\t\t\t\t\tthis.settings.defFileParseConfig.divider\n\t\t\t\t\t\t\t\t\t.underscore,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tcomponent.onChange(async (value) => {\n\t\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t\t!value &&\n\t\t\t\t\t\t\t\t\t!this.settings.defFileParseConfig.divider\n\t\t\t\t\t\t\t\t\t\t.dash\n\t\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\t\tnew Notice(\n\t\t\t\t\t\t\t\t\t\t\"At least one divider must be chosen\",\n\t\t\t\t\t\t\t\t\t\t2000,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\tcomponent.setValue(\n\t\t\t\t\t\t\t\t\t\tthis.settings.defFileParseConfig.divider\n\t\t\t\t\t\t\t\t\t\t\t.underscore,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tthis.settings.defFileParseConfig.divider.underscore =\n\t\t\t\t\t\t\t\t\tvalue;\n\t\t\t\t\t\t\t\tawait this.saveCallback();\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t});\n\t\t\t\t\tmodal.open();\n\t\t\t\t});\n\t\t\t});\n\n\t\tnew Setting(containerEl)\n\t\t\t.setName(\"Default definition file type\")\n\t\t\t.setDesc(\n\t\t\t\t\"When the 'def-type' frontmatter is not specified, the definition file will be treated as this configured default file type.\",\n\t\t\t)\n\t\t\t.addDropdown((component) => {\n\t\t\t\tcomponent.addOption(DefFileType.Consolidated, \"consolidated\");\n\t\t\t\tcomponent.addOption(DefFileType.Atomic, \"atomic\");\n\t\t\t\tcomponent.setValue(\n\t\t\t\t\tthis.settings.defFileParseConfig.defaultFileType ??\n\t\t\t\t\t\tDefFileType.Consolidated,\n\t\t\t\t);\n\t\t\t\tcomponent.onChange(async (val) => {\n\t\t\t\t\tthis.settings.defFileParseConfig.defaultFileType =\n\t\t\t\t\t\tval as DefFileType;\n\t\t\t\t\tawait this.saveCallback();\n\t\t\t\t});\n\t\t\t});\n\n\t\tnew Setting(containerEl)\n\t\t\t.setName(\"Automatically detect plurals -- English only\")\n\t\t\t.setDesc(\n\t\t\t\t\"Attempt to automatically generate aliases for words using English pluralisation rules\",\n\t\t\t)\n\t\t\t.addToggle((component) => {\n\t\t\t\tcomponent.setValue(\n\t\t\t\t\tthis.settings.defFileParseConfig.autoPlurals,\n\t\t\t\t);\n\t\t\t\tcomponent.onChange(async (val) => {\n\t\t\t\t\tthis.settings.defFileParseConfig.autoPlurals = val;\n\t\t\t\t\tawait this.saveCallback();\n\t\t\t\t});\n\t\t\t});\n\n\t\tnew Setting(containerEl)\n\t\t\t.setHeading()\n\t\t\t.setName(\"Definition Popover Settings\");\n\n\t\tnew Setting(containerEl)\n\t\t\t.setName(\"Definition popover display event\")\n\t\t\t.setDesc(\n\t\t\t\t\"Choose the trigger event for displaying the definition popover\",\n\t\t\t)\n\t\t\t.addDropdown((component) => {\n\t\t\t\tcomponent.addOption(PopoverEventSettings.Hover, \"Hover\");\n\t\t\t\tcomponent.addOption(PopoverEventSettings.Click, \"Click\");\n\t\t\t\tcomponent.setValue(this.settings.popoverEvent);\n\t\t\t\tcomponent.onChange(async (value) => {\n\t\t\t\t\tif (\n\t\t\t\t\t\tvalue === PopoverEventSettings.Hover ||\n\t\t\t\t\t\tvalue === PopoverEventSettings.Click\n\t\t\t\t\t) {\n\t\t\t\t\t\tthis.settings.popoverEvent = value;\n\t\t\t\t\t}\n\t\t\t\t\tif (\n\t\t\t\t\t\tthis.settings.popoverEvent ===\n\t\t\t\t\t\tPopoverEventSettings.Click\n\t\t\t\t\t) {\n\t\t\t\t\t\tthis.settings.defPopoverConfig.popoverDismissEvent =\n\t\t\t\t\t\t\tPopoverDismissType.Click;\n\t\t\t\t\t}\n\t\t\t\t\tawait this.saveCallback();\n\t\t\t\t\tthis.display();\n\t\t\t\t});\n\t\t\t});\n\n\t\tif (this.settings.popoverEvent === PopoverEventSettings.Hover) {\n\t\t\tnew Setting(containerEl)\n\t\t\t\t.setName(\"Definition popover dismiss event\")\n\t\t\t\t.setDesc(\n\t\t\t\t\t\"Configure the manner in which you would like to close/dismiss the definition popover.\",\n\t\t\t\t)\n\t\t\t\t.addDropdown((component) => {\n\t\t\t\t\tcomponent.addOption(PopoverDismissType.Click, \"Click\");\n\t\t\t\t\tcomponent.addOption(\n\t\t\t\t\t\tPopoverDismissType.MouseExit,\n\t\t\t\t\t\t\"Mouse exit\",\n\t\t\t\t\t);\n\t\t\t\t\tif (!this.settings.defPopoverConfig.popoverDismissEvent) {\n\t\t\t\t\t\tthis.settings.defPopoverConfig.popoverDismissEvent =\n\t\t\t\t\t\t\tPopoverDismissType.Click;\n\t\t\t\t\t\tthis.saveCallback();\n\t\t\t\t\t}\n\t\t\t\t\tcomponent.setValue(\n\t\t\t\t\t\tthis.settings.defPopoverConfig.popoverDismissEvent,\n\t\t\t\t\t);\n\t\t\t\t\tcomponent.onChange(async (value) => {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tvalue === PopoverDismissType.MouseExit ||\n\t\t\t\t\t\t\tvalue === PopoverDismissType.Click\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tthis.settings.defPopoverConfig.popoverDismissEvent =\n\t\t\t\t\t\t\t\tvalue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawait this.saveCallback();\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t}\n\n\t\tnew Setting(containerEl)\n\t\t\t.setName(\"Display aliases\")\n\t\t\t.setDesc(\n\t\t\t\t\"Display the list of aliases configured for the definition\",\n\t\t\t)\n\t\t\t.addToggle((component) => {\n\t\t\t\tcomponent.setValue(\n\t\t\t\t\tthis.settings.defPopoverConfig.displayAliases,\n\t\t\t\t);\n\t\t\t\tcomponent.onChange(async (value) => {\n\t\t\t\t\tthis.settings.defPopoverConfig.displayAliases = value;\n\t\t\t\t\tawait this.saveCallback();\n\t\t\t\t});\n\t\t\t});\n\n\t\tnew Setting(containerEl)\n\t\t\t.setName(\"Display definition source file\")\n\t\t\t.setDesc(\"Display the title of the definition's source file\")\n\t\t\t.addToggle((component) => {\n\t\t\t\tcomponent.setValue(\n\t\t\t\t\tthis.settings.defPopoverConfig.displayDefFileName,\n\t\t\t\t);\n\t\t\t\tcomponent.onChange(async (value) => {\n\t\t\t\t\tthis.settings.defPopoverConfig.displayDefFileName = value;\n\t\t\t\t\tawait this.saveCallback();\n\t\t\t\t});\n\t\t\t});\n\n\t\tnew Setting(containerEl)\n\t\t\t.setName(\"Custom popover size\")\n\t\t\t.setDesc(\n\t\t\t\t\"Customise the maximum popover size. This is not recommended as it prevents dynamic sizing of the popover based on your viewport.\",\n\t\t\t)\n\t\t\t.addToggle((component) => {\n\t\t\t\tcomponent.setValue(\n\t\t\t\t\tthis.settings.defPopoverConfig.enableCustomSize,\n\t\t\t\t);\n\t\t\t\tcomponent.onChange(async (value) => {\n\t\t\t\t\tthis.settings.defPopoverConfig.enableCustomSize = value;\n\t\t\t\t\tawait this.saveCallback();\n\t\t\t\t\tthis.display();\n\t\t\t\t});\n\t\t\t});\n\n\t\tif (this.settings.defPopoverConfig.enableCustomSize) {\n\t\t\tnew Setting(containerEl)\n\t\t\t\t.setName(\"Popover width (px)\")\n\t\t\t\t.setDesc(\"Maximum width of the definition popover\")\n\t\t\t\t.addSlider((component) => {\n\t\t\t\t\tcomponent.setLimits(150, window.innerWidth, 1);\n\t\t\t\t\tcomponent.setValue(this.settings.defPopoverConfig.maxWidth);\n\t\t\t\t\tcomponent.setDynamicTooltip();\n\t\t\t\t\tcomponent.onChange(async (val) => {\n\t\t\t\t\t\tthis.settings.defPopoverConfig.maxWidth = val;\n\t\t\t\t\t\tawait this.saveCallback();\n\t\t\t\t\t});\n\t\t\t\t});\n\n\t\t\tnew Setting(containerEl)\n\t\t\t\t.setName(\"Popover height (px)\")\n\t\t\t\t.setDesc(\"Maximum height of the definition popover\")\n\t\t\t\t.addSlider((component) => {\n\t\t\t\t\tcomponent.setLimits(150, window.innerHeight, 1);\n\t\t\t\t\tcomponent.setValue(\n\t\t\t\t\t\tthis.settings.defPopoverConfig.maxHeight,\n\t\t\t\t\t);\n\t\t\t\t\tcomponent.setDynamicTooltip();\n\t\t\t\t\tcomponent.onChange(async (val) => {\n\t\t\t\t\t\tthis.settings.defPopoverConfig.maxHeight = val;\n\t\t\t\t\t\tawait this.saveCallback();\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t}\n\n\t\tnew Setting(containerEl)\n\t\t\t.setName(\"Enable definition links\")\n\t\t\t.setDesc(\n\t\t\t\t\"Definitions within popovers will be marked and can be clicked to go to definition.\",\n\t\t\t)\n\t\t\t.addToggle((component) => {\n\t\t\t\tcomponent.setValue(\n\t\t\t\t\tthis.settings.defPopoverConfig.enableDefinitionLink,\n\t\t\t\t);\n\t\t\t\tcomponent.onChange(async (val) => {\n\t\t\t\t\tthis.settings.defPopoverConfig.enableDefinitionLink = val;\n\t\t\t\t\tawait this.saveCallback();\n\t\t\t\t});\n\t\t\t});\n\n\t\tnew Setting(containerEl)\n\t\t\t.setName(\"Background colour\")\n\t\t\t.setDesc(\n\t\t\t\t\"Customise the background colour of the definition popover\",\n\t\t\t)\n\t\t\t.addExtraButton((component) => {\n\t\t\t\tcomponent.setIcon(\"rotate-ccw\");\n\t\t\t\tcomponent.setTooltip(\"Reset to default colour set by theme\");\n\t\t\t\tcomponent.onClick(async () => {\n\t\t\t\t\tthis.settings.defPopoverConfig.backgroundColour = undefined;\n\t\t\t\t\tawait this.saveCallback();\n\t\t\t\t\tthis.display();\n\t\t\t\t});\n\t\t\t})\n\t\t\t.addColorPicker((component) => {\n\t\t\t\tif (this.settings.defPopoverConfig.backgroundColour) {\n\t\t\t\t\tcomponent.setValue(\n\t\t\t\t\t\tthis.settings.defPopoverConfig.backgroundColour,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tcomponent.onChange(async (val) => {\n\t\t\t\t\tthis.settings.defPopoverConfig.backgroundColour = val;\n\t\t\t\t\tawait this.saveCallback();\n\t\t\t\t});\n\t\t\t});\n\t}\n}\n\nexport function getSettings(): Settings {\n\treturn window.NoteDefinition.settings;\n}\n"
  },
  {
    "path": "src/tests/consolidated-def-parser.test.ts",
    "content": "import { App, TFile } from \"obsidian\";\nimport { ConsolidatedDefParser } from \"src/core/consolidated-def-parser\";\nimport { DefFileType } from \"src/core/file-type\";\nimport { DefFileParseConfig } from \"src/settings\";\n\nconst fs = require(\"node:fs\");\n\n// Setup for test file\nconst consolidatedDefData = fs.readFileSync(\n\t\"src/tests/def-file-samples/consolidated-definitions-test.md\",\n\t\"utf8\",\n);\n\nconst caseSensitiveDefData = fs.readFileSync(\n\t\"src/tests/def-file-samples/case-sensitve-definitions-test.md\",\n\t\"utf8\",\n);\n\nconst consolidatedTrainingWhitespace = fs.readFileSync(\n\t\"src/tests/def-file-samples/consolidated-trailing-whitespace.md\",\n\t\"utf8\",\n);\n\nconst consolidatedStartFileWhitespace = fs.readFileSync(\n\t\"src/tests/def-file-samples/consolidated-start-of-file-whitespace.md\",\n\t\"utf8\",\n);\n\nconst parseSettings: DefFileParseConfig = {\n\tdefaultFileType: DefFileType.Consolidated,\n\tdivider: {\n\t\tunderscore: true,\n\t\tdash: true,\n\t},\n\tautoPlurals: false,\n\tenableCaseSensitive: false,\n};\n\nconst caseSensitiveParseSettings: DefFileParseConfig = {\n\tdefaultFileType: DefFileType.Consolidated,\n\tdivider: {\n\t\tunderscore: true,\n\t\tdash: true,\n\t},\n\tautoPlurals: false,\n\tenableCaseSensitive: true,\n};\n\nconst file = {\n\tpath: \"src/tests/consolidated-definitions-test.md\",\n};\n\nconst parser = new ConsolidatedDefParser(\n\tnull as unknown as App,\n\tfile as TFile,\n\tparseSettings,\n);\n\nconst caseSensitiveParser = new ConsolidatedDefParser(\n\tnull as unknown as App,\n\tfile as TFile,\n\tcaseSensitiveParseSettings,\n);\n\nconst definitions = parser.directParseFile(consolidatedDefData);\nconst caseSensitiveDefinitions =\n\tcaseSensitiveParser.directParseFile(caseSensitiveDefData);\n\ndescribe(\"Valid definition file can be parsed correctly\", () => {\n\tit(\"Words of definitions are parsed correctly\", async () => {\n\t\texpect(definitions.find((def) => def.word === \"First\")).toBeDefined();\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.word === \"Multiple-word definition\"),\n\t\t).toBeDefined();\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.word === \"Alias definition\"),\n\t\t).toBeDefined();\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.word === \"Markdown support\"),\n\t\t).toBeDefined();\n\t});\n\n\tit(\"Keys are stored as lowercase of words when case-sensitve disabled\", () => {\n\t\texpect(definitions.find((def) => def.word === \"First\")?.key).toBe(\n\t\t\t\"first\",\n\t\t);\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.word === \"Multiple-word definition\")\n\t\t\t\t?.key,\n\t\t).toBe(\"multiple-word definition\");\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.word === \"Alias definition\")?.key,\n\t\t).toBe(\"alias definition\");\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.word === \"Markdown support\")?.key,\n\t\t).toBe(\"markdown support\");\n\t});\n\n\tit(\"Definitions are parsed correctly\", () => {\n\t\texpect(definitions.find((def) => def.key === \"first\")?.definition).toBe(\n\t\t\t\"This is the first definition to test basic functionality.\",\n\t\t);\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.key === \"multiple-word definition\")\n\t\t\t\t?.definition,\n\t\t).toBe(\"This ensures that multiple-word definitions works.\");\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.key === \"alias definition\")\n\t\t\t\t?.definition,\n\t\t).toBe(\"This tests if the alias function works.\");\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.key === \"markdown support\")\n\t\t\t\t?.definition,\n\t\t).toBe(\"Markdown syntax _should_ *work*.\");\n\t});\n\n\tit(\"Positions are parsed correctly\", () => {\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.key === \"first\")?.position?.from,\n\t\t).toBe(0);\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.key === \"first\")?.position?.to,\n\t\t).toBe(4);\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.key === \"multiple-word definition\")\n\t\t\t\t?.position?.from,\n\t\t).toBe(6);\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.key === \"multiple-word definition\")\n\t\t\t\t?.position?.to,\n\t\t).toBe(10);\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.key === \"alias definition\")?.position\n\t\t\t\t?.from,\n\t\t).toBe(12);\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.key === \"alias definition\")?.position\n\t\t\t\t?.to,\n\t\t).toBe(18);\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.key === \"markdown support\")?.position\n\t\t\t\t?.from,\n\t\t).toBe(20);\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.key === \"markdown support\")?.position\n\t\t\t\t?.to,\n\t\t).toBe(22);\n\t});\n\n\tit(\"Aliases are parsed correctly\", () => {\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.key === \"alias definition\")?.aliases,\n\t\t).toStrictEqual([\"Alias1\", \"Alias2\"]);\n\t});\n});\n\ndescribe(\"Consolidated definition file has odd formatting, but still valid syntax\", () => {\n\tit(\"Extra end of file whitespace characters should be ignored\", () => {\n\t\tconst file = {\n\t\t\tpath: \"src/tests/consolidated-trailing-whitespace.md\",\n\t\t};\n\t\tconst parser = new ConsolidatedDefParser(\n\t\t\tnull as unknown as App,\n\t\t\tfile as TFile,\n\t\t\tparseSettings,\n\t\t);\n\n\t\tconst definitions = parser.directParseFile(\n\t\t\tconsolidatedTrainingWhitespace,\n\t\t);\n\t\texpect(definitions.find((def) => def.word === \"First\")).toBeDefined();\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.word === \"Multiple-word definition\"),\n\t\t).toBeDefined();\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.word === \"Alias definition\"),\n\t\t).toBeDefined();\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.word === \"Markdown support\"),\n\t\t).toBeDefined();\n\t});\n\n\tit(\"Start of file whitespace should be ignored\", () => {\n\t\tconst file = {\n\t\t\tpath: \"src/tests/consolidated-start-of-file-whitespace.md\",\n\t\t};\n\t\tconst parser = new ConsolidatedDefParser(\n\t\t\tnull as unknown as App,\n\t\t\tfile as TFile,\n\t\t\tparseSettings,\n\t\t);\n\t\tconst definitions = parser.directParseFile(\n\t\t\tconsolidatedStartFileWhitespace,\n\t\t);\n\t\texpect(definitions.find((def) => def.word === \"First\")).toBeDefined();\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.word === \"Multiple-word definition\"),\n\t\t).toBeDefined();\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.word === \"Alias definition\"),\n\t\t).toBeDefined();\n\t\texpect(\n\t\t\tdefinitions.find((def) => def.word === \"Markdown support\"),\n\t\t).toBeDefined();\n\t});\n});\n\ndescribe(\"Valid definition file can be parsed correctly when case-sensitive enabled\", () => {\n\tit(\"Keys are stored with correct case when case-sensitive enabled\", () => {\n\t\texpect(\n\t\t\tcaseSensitiveDefinitions.find((def) => def.word === \"First\")?.key,\n\t\t).toBe(\"First\");\n\t\texpect(\n\t\t\tcaseSensitiveDefinitions.find((def) => def.word === \"first\")?.key,\n\t\t).toBe(\"first\");\n\t\texpect(\n\t\t\tcaseSensitiveDefinitions.find(\n\t\t\t\t(def) => def.word === \"Multiple-word definition\",\n\t\t\t)?.key,\n\t\t).toBe(\"Multiple-word definition\");\n\t\texpect(\n\t\t\tcaseSensitiveDefinitions.find(\n\t\t\t\t(def) => def.word === \"Multiple-word Definition\",\n\t\t\t)?.key,\n\t\t).toBe(\"Multiple-word Definition\");\n\t\texpect(\n\t\t\tcaseSensitiveDefinitions.find(\n\t\t\t\t(def) => def.word === \"Alias definition\",\n\t\t\t)?.key,\n\t\t).toBe(\"Alias definition\");\n\t\texpect(\n\t\t\tcaseSensitiveDefinitions.find(\n\t\t\t\t(def) => def.word === \"alias definition\",\n\t\t\t)?.key,\n\t\t).toBeUndefined;\n\t\texpect(\n\t\t\tcaseSensitiveDefinitions.find(\n\t\t\t\t(def) => def.word === \"Markdown support\",\n\t\t\t)?.key,\n\t\t).toBe(\"Markdown support\");\n\t\texpect(\n\t\t\tcaseSensitiveDefinitions.find(\n\t\t\t\t(def) => def.word === \"markdown support\",\n\t\t\t)?.key,\n\t\t).toBeUndefined;\n\t});\n\n\tit(\"Definitions are parsed correctly when case-sensitive enabled\", () => {\n\t\texpect(\n\t\t\tcaseSensitiveDefinitions.find((def) => def.key === \"first\")\n\t\t\t\t?.definition,\n\t\t).toBe(\"This is the first definition to test basic functionality.\");\n\t\texpect(\n\t\t\tcaseSensitiveDefinitions.find((def) => def.key === \"First\")\n\t\t\t\t?.definition,\n\t\t).toBe(\"This is a different definition than the first.\");\n\t\texpect(\n\t\t\tcaseSensitiveDefinitions.find(\n\t\t\t\t(def) => def.key === \"Multiple-word definition\",\n\t\t\t)?.definition,\n\t\t).toBe(\"This ensures that multiple-word definitions works.\");\n\t\texpect(\n\t\t\tcaseSensitiveDefinitions.find(\n\t\t\t\t(def) => def.key === \"Multiple-word Definition\",\n\t\t\t)?.definition,\n\t\t).toBe(\"This ensures that case matters in multiple word definitions.\");\n\t\texpect(\n\t\t\tcaseSensitiveDefinitions.find(\n\t\t\t\t(def) => def.key === \"Alias definition\",\n\t\t\t)?.definition,\n\t\t).toBe(\"This tests if the alias function works.\");\n\t\texpect(\n\t\t\tcaseSensitiveDefinitions.find(\n\t\t\t\t(def) => def.key === \"Markdown support\",\n\t\t\t)?.definition,\n\t\t).toBe(\"Markdown syntax _should_ *work*.\");\n\t});\n});\n"
  },
  {
    "path": "src/tests/decorator.test.ts",
    "content": "import { scanText } from \"src/editor/decoration\";\nimport { PhraseInfo } from \"src/editor/definition-search\";\nimport { PTreeNode } from \"src/editor/prefix-tree\";\n\njest.mock(\"src/settings\", () => ({\n\tgetSettings: jest.fn(() => ({\n\t\tdefFileParseConfig: {\n\t\t\tenableCaseSensitive: false,\n\t\t},\n\t})),\n}));\nafterEach(() => {\n\tjest.clearAllMocks();\n});\n\nconst pTree = new PTreeNode();\npTree.add(\"word1\");\npTree.add(\"word2\");\npTree.add(\"a phrase\");\npTree.add(\"a long phrase\");\npTree.add(\"long\");\n\ntest(\"Defined words are correctly detected in a simple sentence\", () => {\n\tconst text =\n\t\t\"Hi this is a simple sentence with word1, word2 and a phrase defined.\";\n\tconst phraseInfo = scanText(text, 0, pTree);\n\tconst expectedPhraseInfo: PhraseInfo[] = [\n\t\t{\n\t\t\tfrom: 34,\n\t\t\tto: 39,\n\t\t\tphrase: \"word1\",\n\t\t},\n\t\t{\n\t\t\tfrom: 41,\n\t\t\tto: 46,\n\t\t\tphrase: \"word2\",\n\t\t},\n\t\t{\n\t\t\tfrom: 51,\n\t\t\tto: 59,\n\t\t\tphrase: \"a phrase\",\n\t\t},\n\t];\n\texpect(phraseInfo).toStrictEqual(expectedPhraseInfo);\n});\n\ntest(\"Defined words are correctly detected in a paragraph\", () => {\n\tconst text = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \nUt enim ad minim veniam, word1 quis nostrud exercitation ullamco laboris nisi word2 ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in a phrase voluptate velit esse cillum dolore eu fugiat nulla pariatur. \nExcepteur sint occaecat cupidatat non proident, sunt word2 in culpa qui officia deserunt mollit anim id word1 est laborum`;\n\n\tconst phraseInfo = scanText(text, 0, pTree);\n\tconst expectedPhraseInfo = [\n\t\t{ phrase: \"word1\", from: 150, to: 155 },\n\t\t{ phrase: \"word2\", from: 203, to: 208 },\n\t\t{ phrase: \"a phrase\", from: 287, to: 295 },\n\t\t{ phrase: \"word2\", from: 411, to: 416 },\n\t\t{ phrase: \"word1\", from: 462, to: 467 },\n\t];\n\n\texpect(phraseInfo).toStrictEqual(expectedPhraseInfo);\n});\n\ntest(\"Offset is correctly added to positions\", () => {\n\tconst text =\n\t\t\"Hi this is a simple sentence with word1, word2 and a phrase defined.\";\n\tconst phraseInfo = scanText(text, 2, pTree);\n\tconst expectedPhraseInfo: PhraseInfo[] = [\n\t\t{\n\t\t\tfrom: 36,\n\t\t\tto: 41,\n\t\t\tphrase: \"word1\",\n\t\t},\n\t\t{\n\t\t\tfrom: 43,\n\t\t\tto: 48,\n\t\t\tphrase: \"word2\",\n\t\t},\n\t\t{\n\t\t\tfrom: 53,\n\t\t\tto: 61,\n\t\t\tphrase: \"a phrase\",\n\t\t},\n\t];\n\texpect(phraseInfo).toStrictEqual(expectedPhraseInfo);\n});\n\ntest(\"Definitions that are a subset of another are detected correctly. The longer definition is preferred.\", () => {\n\tconst text =\n\t\t\"Although longer definitions are preferred in a long phrase. The long word should still be normally detected\";\n\tconst phraseInfo = scanText(text, 0, pTree);\n\tconst expectedPhraseInfo = [\n\t\t{ phrase: \"a long phrase\", from: 45, to: 58 },\n\t\t{ phrase: \"long\", from: 64, to: 68 },\n\t];\n\texpect(phraseInfo).toStrictEqual(expectedPhraseInfo);\n});\n"
  },
  {
    "path": "src/tests/def-file-samples/case-sensitve-definitions-test.md",
    "content": "# first\n\nThis is the first definition to test basic functionality.\n\n---\n\n# First\n\nThis is a different definition than the first.\n\n---\n\n# Multiple-word definition\n\nThis ensures that multiple-word definitions works.\n\n---\n\n# Multiple-word Definition\n\nThis ensures that case matters in multiple word definitions.\n\n---\n\n# Alias definition\n\n*Alias1, alias2*\n\nThis tests if the alias function works.\n\n---\n\n# Markdown support\n\nMarkdown syntax _should_ *work*.\n"
  },
  {
    "path": "src/tests/def-file-samples/consolidated-definitions-test.md",
    "content": "# First\n\nThis is the first definition to test basic functionality.\n\n---\n\n# Multiple-word definition\n\nThis ensures that multiple-word definitions works.\n\n---\n\n# Alias definition\n\n*Alias1, Alias2*\n\nThis tests if the alias function works.\n\n---\n\n# Markdown support\n\nMarkdown syntax _should_ *work*.\n"
  },
  {
    "path": "src/tests/def-file-samples/consolidated-start-of-file-whitespace.md",
    "content": "\n\n\n\n\n\n \n # First\n\nThis is the first definition to test basic functionality.\n\n---\n\n# Multiple-word definition\n\nThis ensures that multiple-word definitions works.\n\n---\n\n# Alias definition\n\n*Alias1, Alias2*\n\nThis tests if the alias function works.\n\n---\n\n# Markdown support\n\nMarkdown syntax _should_ *work*.\n\n"
  },
  {
    "path": "src/tests/def-file-samples/consolidated-trailing-delimiter.md",
    "content": "# First\n\nThis is the first definition to test basic functionality.\n\n---\n\n# Multiple-word definition\n\nThis ensures that multiple-word definitions works.\n\n---\n\n# Alias definition\n\n*Alias1, Alias2*\n\nThis tests if the alias function works.\n\n---\n\n# Markdown support\n\nMarkdown syntax _should_ *work*.\n\n   \n--- \n\n"
  },
  {
    "path": "src/tests/def-file-samples/consolidated-trailing-whitespace.md",
    "content": "# First\n\nThis is the first definition to test basic functionality.\n\n---\n\n# Multiple-word definition\n\nThis ensures that multiple-word definitions works.\n\n---\n\n# Alias definition\n\n*Alias1, Alias2*\n\nThis tests if the alias function works.\n\n---\n\n# Markdown support\n\nMarkdown syntax _should_ *work*.\n\n   \n"
  },
  {
    "path": "src/tests/def-file-updater.test.ts",
    "content": "import { App, TFile } from \"obsidian\";\nimport { DefFileUpdater } from \"src/core/def-file-updater\";\nimport { DefFileType } from \"src/core/file-type\";\nimport { DefManager } from \"__mocks__/internals\";\n\njest.mock(\"src/core/def-file-manager\", () => {\n\treturn {\n\t\tgetDefFileManager: () => new DefManager(),\n\t};\n});\n\njest.mock(\"src/settings\", () => ({\n\tgetSettings: jest.fn(() => ({\n\t\tdefFileParseConfig: {\n\t\t\tdivider: {\n\t\t\t\tdash: true,\n\t\t\t\tunderscore: false,\n\t\t\t},\n\t\t},\n\t})),\n}));\n\njest.mock(\"src/util/log\");\n\nconst app = new App();\nconst defFileUpdater = new DefFileUpdater(app);\n\nconst vaultModify = jest.spyOn(app.vault, \"modify\");\n\nafterEach(() => {\n\tjest.clearAllMocks();\n});\n\ntest(\"Update atomic definition\", async () => {\n\tconst file = {\n\t\tbasename: \"atomic\",\n\t\textension: \"md\",\n\t} as TFile;\n\tawait defFileUpdater.updateDefinition({\n\t\tkey: \"atomic\",\n\t\tword: \"atomic\",\n\t\taliases: [],\n\t\tdefinition: \"this is a test definition\",\n\t\tfile: file,\n\t\tlinkText: \"\",\n\t\tfileType: DefFileType.Atomic,\n\t});\n\texpect(vaultModify).toHaveBeenCalledWith(file, \"this is a test definition\");\n});\n\ndescribe(\"Test modifying consolidated file\", () => {\n\tit(\"Update consolidated definition\", async () => {\n\t\tconst file = {\n\t\t\tbasename: \"consolidated\",\n\t\t\textension: \"md\",\n\t\t} as TFile;\n\n\t\tconst oldContent = `# oldWord\n\n*oldAlias*\n\noldDefinition\n\n---\n\n# Another Definition\n\nanotherDefValue\n\n---\n\n# Yet another def\n\nYet another definition`;\n\t\tconst newDefinitionText = \"This is a definition, blah blah blah.\";\n\t\tconst expectedNewContent = `# oldWord\n\n*oldAlias*\n\nThis is a definition, blah blah blah.\n\n---\n\n# Another Definition\n\nanotherDefValue\n\n---\n\n# Yet another def\n\nYet another definition`;\n\n\t\tjest.spyOn(app.vault, \"read\").mockResolvedValue(oldContent);\n\t\tjest.spyOn(app.metadataCache, \"getFileCache\").mockReturnValue({});\n\n\t\tawait defFileUpdater.updateDefinition({\n\t\t\tkey: \"oldword\",\n\t\t\tword: \"oldWord\",\n\t\t\taliases: [\"oldAlias\"],\n\t\t\tdefinition: newDefinitionText,\n\t\t\tfile: file,\n\t\t\tlinkText: \"\",\n\t\t\tfileType: DefFileType.Consolidated,\n\t\t});\n\n\t\texpect(vaultModify).toHaveBeenCalledWith(file, expectedNewContent);\n\t});\n\n\tit(\"Add definition to consolidated file\", async () => {\n\t\tconst file = {\n\t\t\tbasename: \"consolidated\",\n\t\t\textension: \"md\",\n\t\t} as TFile;\n\n\t\tconst oldContent = `---\ndef-type: consolidated\n---\n\n# Existing Def\nExisting definition.\n`;\n\t\tconst newDef = {\n\t\t\tword: \"New Def\",\n\t\t\taliases: [\"New Alias\"],\n\t\t\tdefinition: \"This is a new definition.\",\n\t\t\tfile: file,\n\t\t\tfileType: DefFileType.Consolidated,\n\t\t};\n\t\tconst expectedNewContent = `---\ndef-type: consolidated\n---\n# Existing Def\n\nExisting definition.\n\n---\n\n# New Def\n\n*New Alias*\n\nThis is a new definition.`;\n\n\t\tjest.spyOn(app.vault, \"read\").mockResolvedValue(oldContent);\n\t\tjest.spyOn(app.metadataCache, \"getFileCache\").mockReturnValue({\n\t\t\tfrontmatterPosition: {\n\t\t\t\tstart: {\n\t\t\t\t\tline: 0,\n\t\t\t\t\tcol: 0,\n\t\t\t\t\toffset: 0,\n\t\t\t\t},\n\t\t\t\tend: {\n\t\t\t\t\tline: 2,\n\t\t\t\t\tcol: 3,\n\t\t\t\t\toffset: 30,\n\t\t\t\t},\n\t\t\t},\n\t\t});\n\n\t\tawait defFileUpdater.addDefinition(newDef);\n\n\t\texpect(vaultModify).toHaveBeenCalledWith(file, expectedNewContent);\n\t});\n\n\tit(\"Add definition to empty file\", async () => {\n\t\tconst file = {\n\t\t\tbasename: \"consolidated\",\n\t\t\textension: \"md\",\n\t\t} as TFile;\n\n\t\tconst oldContent = ``;\n\t\tconst newDef = {\n\t\t\tword: \"New Def\",\n\t\t\taliases: [\"New Alias\"],\n\t\t\tdefinition: \"This is a new definition.\",\n\t\t\tfile: file,\n\t\t\tfileType: DefFileType.Consolidated,\n\t\t};\n\t\tconst expectedNewContent = `# New Def\n\n*New Alias*\n\nThis is a new definition.`;\n\n\t\tjest.spyOn(app.vault, \"read\").mockResolvedValue(oldContent);\n\n\t\tawait defFileUpdater.addDefinition(newDef);\n\n\t\texpect(vaultModify).toHaveBeenCalledWith(file, expectedNewContent);\n\t});\n});\n"
  },
  {
    "path": "src/types/obsidian.d.ts",
    "content": "import { View } from \"obsidian\";\n\ninterface FileExplorerView extends View {\n\tfileItems: { [key: string]: FileItem };\n}\n\ninterface FileItem {\n\tselfEl: HTMLElement;\n\tinnerEl: HTMLElement;\n}\n"
  },
  {
    "path": "src/ui/file-explorer.ts",
    "content": "import { App } from \"obsidian\";\nimport {\n\tDEFAULT_DEF_FOLDER,\n\tgetSettings,\n\tVALID_DEFINITION_FILE_TYPES,\n} from \"src/settings\";\nimport { FileExplorerView } from \"src/types/obsidian\";\nimport { logDebug } from \"src/util/log\";\n\nlet fileExplorerDecoration: FileExplorerDecoration;\n\nconst MAX_RETRY = 3;\nconst RETRY_INTERVAL = 1000;\nconst DIV_ID = \"def-tag-id\";\n\nexport class FileExplorerDecoration {\n\tapp: App;\n\tretryCount: number;\n\n\tconstructor(app: App) {\n\t\tthis.app = app;\n\t}\n\n\t// Take note: May not be re-entrant\n\tasync run() {\n\t\tthis.retryCount = 0;\n\n\t\t// Retry required as some views may not be loaded on initial app start\n\t\twhile (this.retryCount < MAX_RETRY) {\n\t\t\ttry {\n\t\t\t\tthis.exec();\n\t\t\t} catch (e) {\n\t\t\t\tlogDebug(e);\n\t\t\t\tthis.retryCount++;\n\t\t\t\tawait sleep(RETRY_INTERVAL);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t}\n\n\tprivate exec() {\n\t\tconst fileExplorer =\n\t\t\tthis.app.workspace.getLeavesOfType(\"file-explorer\")[0];\n\t\tif (!fileExplorer) {\n\t\t\t// This is an expected behaviour, likely due to\n\t\t\tthrow new Error(\n\t\t\t\t\"app.workspace.getLeavesOfType('file-explorer') returned undefined (file explorer may not be available in view yet)\",\n\t\t\t);\n\t\t}\n\t\tconst fileExpView = fileExplorer.view as FileExplorerView;\n\n\t\tconst settings = getSettings();\n\t\tObject.keys(fileExpView.fileItems).forEach((k) => {\n\t\t\tconst fileItem = fileExpView.fileItems[k];\n\n\t\t\t// Clear previously added ones (if exist)\n\t\t\tconst fileTags =\n\t\t\t\tfileItem.selfEl.getElementsByClassName(\"nav-file-tag\");\n\t\t\tfor (let i = 0; i < fileTags.length; i++) {\n\t\t\t\tconst fileTag = fileTags[i];\n\t\t\t\tif (fileTag.id === DIV_ID) {\n\t\t\t\t\tfileTag.remove();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst defFolder = settings.defFolder || DEFAULT_DEF_FOLDER;\n\n\t\t\t// If def folder is an invalid folder path, then do not add any tags\n\t\t\tif (!fileExpView.fileItems[defFolder]) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\tk.startsWith(defFolder) &&\n\t\t\t\tVALID_DEFINITION_FILE_TYPES.some((ext) => k.endsWith(ext))\n\t\t\t) {\n\t\t\t\tthis.tagFile(fileExpView, k, \"DEF\");\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate tagFile(\n\t\texplorer: FileExplorerView,\n\t\tfilePath: string,\n\t\ttagContent: string,\n\t) {\n\t\tconst el = explorer.fileItems[filePath];\n\t\tif (!el) {\n\t\t\tlogDebug(`No file item with filepath ${filePath} found`);\n\t\t\treturn;\n\t\t}\n\n\t\tconst fileTags = el.selfEl.getElementsByClassName(\"nav-file-tag\");\n\t\tfor (let i = 0; i < fileTags.length; i++) {\n\t\t\tconst fileTag = fileTags[i];\n\t\t\tfileTag.remove();\n\t\t}\n\n\t\tel.selfEl.createDiv({\n\t\t\tcls: \"nav-file-tag\",\n\t\t\ttext: tagContent,\n\t\t\tattr: {\n\t\t\t\tid: DIV_ID,\n\t\t\t},\n\t\t});\n\t}\n}\n\nexport function initFileExplorerDecoration(app: App): FileExplorerDecoration {\n\tfileExplorerDecoration = new FileExplorerDecoration(app);\n\treturn fileExplorerDecoration;\n}\n\nexport function getFileExplorerDecoration(app: App): FileExplorerDecoration {\n\tif (fileExplorerDecoration) {\n\t\treturn fileExplorerDecoration;\n\t}\n\treturn initFileExplorerDecoration(app);\n}\n"
  },
  {
    "path": "src/util/editor.ts",
    "content": "import { Editor } from \"obsidian\";\nimport { getMarkedPhrases } from \"src/editor/decoration\";\nimport { getSettings } from \"src/settings\";\n\n\nexport function getMarkedWordUnderCursor(editor: Editor) {\n\tconst currWord = getWordByOffset(editor.posToOffset(editor.getCursor()));\n\treturn normaliseWord(currWord);\n}\n\nexport function normaliseWord(word: string) {\n\tif (getSettings().defFileParseConfig.enableCaseSensitive)\n\t\treturn word.trimStart().trimEnd();\n\telse\n\t\treturn word.trimStart().trimEnd().toLowerCase();\n}\n\nfunction getWordByOffset(offset: number): string {\n\tconst markedPhrases = getMarkedPhrases();\n\tlet start = 0;\n\tlet end = markedPhrases.length - 1;\n\n\t// Binary search to get marked word at provided position\n\twhile (start <= end) {\n\t\tlet mid = Math.floor((start + end) / 2);\n\n\t\tlet currPhrase = markedPhrases[mid];\n\t\tif (offset >= currPhrase.from && offset <= currPhrase.to) {\n\t\t\treturn currPhrase.phrase;\n\t\t}\n\t\tif (offset < currPhrase.from) {\n\t\t\tend = mid - 1;\n\t\t}\n\t\tif (offset > currPhrase.to) {\n\t\t\tstart = mid + 1;\n\t\t}\n\t}\n\treturn \"\";\n}\n"
  },
  {
    "path": "src/util/log.ts",
    "content": "// Rudimentary logger implementation\n\nexport enum LogLevel {\n\tSilent,\n\tError,\n\tWarn,\n\tInfo,\n\tDebug,\n}\n\nconst levelMap = {\n\t0: \"SILENT\", // Should not be used\n\t1: \"ERROR\",\n\t2: \"WARN\",\n\t3: \"INFO\",\n\t4: \"DEBUG\",\n};\n\n// Log only if current log level is >= specified log level\nfunction logWithLevel(msg: string, logLevel: LogLevel) {\n\tif (window.NoteDefinition.LOG_LEVEL >= logLevel) {\n\t\tconsole.log(`${levelMap[logLevel]}: ${msg}`);\n\t}\n}\n\n// Convenience methods for each level\n\nexport function logDebug(msg: string) {\n\tlogWithLevel(msg, LogLevel.Debug);\n}\n\nexport function logInfo(msg: string) {\n\tlogWithLevel(msg, LogLevel.Info);\n}\n\nexport function logWarn(msg: string) {\n\tlogWithLevel(msg, LogLevel.Warn);\n}\n\nexport function logError(msg: string) {\n\tlogWithLevel(msg, LogLevel.Error);\n}\n"
  },
  {
    "path": "src/util/retry.ts",
    "content": "const RETRY_INTERVAL = 1000;\n\nexport function useRetry(retryCount?: number) {\n\tlet shouldRetry = false;\n\tlet maxRetries = retryCount ?? 3;\n\tlet currRetry = 0;\n\n\tasync function exec(func: any) {\n\t\twhile (currRetry < maxRetries) {\n\t\t\tconst output = func();\n\t\t\tif (!shouldRetry) {\n\t\t\t\treturn output;\n\t\t\t}\n\t\t\tshouldRetry = false;\n\t\t\tcurrRetry++;\n\t\t\tawait sleep(RETRY_INTERVAL);\n\t\t}\n\t\tthrow new Error(\"Failed to exec function, hit max retries\");\n\t}\n\n\tfunction setShouldRetry() {\n\t\tshouldRetry = true;\n\t}\n\n\treturn {\n\t\texec,\n\t\tsetShouldRetry,\n\t};\n}\n"
  },
  {
    "path": "styles.css",
    "content": ".definition-popover {\n\tbackground-color: var(--background-secondary);\n\tborder: 1px solid var(--background-modifier-border-hover);\n\tborder-radius: var(--radius-m);\n\tposition: absolute;\n\tpadding: var(--size-4-2) var(--size-4-3);\n\tbox-shadow: var(--shadow-s);\n\tmin-height: 100px;\n\tmin-width: 150px;\n\toverflow: auto;\n}\n\n.definition-popover-filename {\n\tcolor: var(--text-faint);\n\tfloat: right;\n}\n\n.def-decoration {\n\ttext-decoration: underline var(--color-yellow) dotted;\n\t-webkit-text-decoration: underline var(--color-yellow) dotted;\n}\n\n.def-link-decoration {\n\ttext-decoration: underline var(--color-green) dotted;\n\t-webkit-text-decoration: underline var(--color-green) dotted;\n\tcursor: pointer;\n}\n\n.edit-modal-section-header {\n\tmargin-top: 5px;\n\tmargin-bottom: 5px;\n\tcolor: var(--text-muted);\n}\n\n.edit-modal-aliases {\n\twidth: 100%;\n\tresize: none;\n\tfont-size: var(--font-ui-medium);\n\theight: 2em;\n}\n\n.edit-modal-textarea {\n\twidth: 100%;\n\theight: 20vh;\n\tresize: none;\n\tfont-size: var(--font-ui-medium);\n\tmargin-bottom: 10px;\n}\n\n.edit-modal-save-button {\n\tfont-size: var(--font-ui-medium);\n\tfloat: right;\n\tbackground-color: var(--interactive-normal);\n}\n\n.popover-go-to-def-button {\n\tposition: absolute;\n\ttop: 1em;\n\tright: 1em;\n\n\tbackground-color: var(--interactive-normal);\n\topacity: 0.2;\n\ttransition-duration: 0.1s;\n}\n.popover-go-to-def-button:hover {\n\topacity: 1;\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n\t\"compilerOptions\": {\n\t\t\"baseUrl\": \".\",\n\t\t\"esModuleInterop\": true,\n\t\t\"inlineSourceMap\": true,\n\t\t\"inlineSources\": true,\n\t\t\"module\": \"ESNext\",\n\t\t\"target\": \"ES6\",\n\t\t\"allowJs\": true,\n\t\t\"noImplicitAny\": true,\n\t\t\"moduleResolution\": \"node\",\n\t\t\"importHelpers\": true,\n\t\t\"isolatedModules\": true,\n\t\t\"strictNullChecks\": true,\n\t\t\"lib\": [\"DOM\", \"ES5\", \"ES6\", \"ES7\"]\n\t},\n\t\"include\": [\"**/*.ts\"]\n}\n"
  },
  {
    "path": "version-bump.mjs",
    "content": "import { readFileSync, writeFileSync } from \"fs\";\n\nconst targetVersion = process.env.npm_package_version;\n\n// read minAppVersion from manifest.json and bump version to target version\nlet manifest = JSON.parse(readFileSync(\"manifest.json\", \"utf8\"));\nconst { minAppVersion } = manifest;\nmanifest.version = targetVersion;\nwriteFileSync(\"manifest.json\", JSON.stringify(manifest, null, \"\\t\"));\n\n// update versions.json with target version and minAppVersion from manifest.json\nlet versions = JSON.parse(readFileSync(\"versions.json\", \"utf8\"));\nversions[targetVersion] = minAppVersion;\nwriteFileSync(\"versions.json\", JSON.stringify(versions, null, \"\\t\"));\n"
  },
  {
    "path": "versions.json",
    "content": "{\n\t\"0.0.4\": \"1.5.12\",\n\t\"0.1.0\": \"1.5.12\",\n\t\"0.1.1\": \"1.5.12\",\n\t\"0.2.0\": \"1.5.12\",\n\t\"0.2.1\": \"1.5.12\",\n\t\"0.3.0\": \"1.5.12\",\n\t\"0.4.0\": \"1.5.12\",\n\t\"0.5.0\": \"1.5.12\",\n\t\"0.6.0\": \"1.5.12\",\n\t\"0.7.0\": \"1.5.12\",\n\t\"0.8.0\": \"1.5.12\",\n\t\"0.9.0\": \"1.5.12\",\n\t\"0.9.1\": \"1.5.12\",\n\t\"0.9.2\": \"1.5.12\",\n\t\"0.9.3\": \"1.5.12\",\n\t\"0.10.0\": \"1.5.12\",\n\t\"0.10.1\": \"1.5.12\",\n\t\"0.10.2\": \"1.5.12\",\n\t\"0.10.3\": \"1.5.12\",\n\t\"0.11.0\": \"1.5.12\",\n\t\"0.12.0\": \"1.5.12\",\n\t\"0.12.1\": \"1.5.12\",\n\t\"0.13.0\": \"1.5.12\",\n\t\"0.13.1\": \"1.5.12\",\n\t\"0.14.0\": \"1.5.12\",\n\t\"0.14.1\": \"1.5.12\",\n\t\"0.15.0\": \"1.5.12\",\n\t\"0.16.0\": \"1.5.12\",\n\t\"0.16.1\": \"1.5.12\",\n\t\"0.16.2\": \"1.5.12\",\n\t\"0.17.0\": \"1.5.12\",\n\t\"0.17.1\": \"1.5.12\",\n\t\"0.18.0\": \"1.5.12\",\n\t\"0.18.1\": \"1.5.12\",\n\t\"0.19.0\": \"1.5.12\",\n\t\"0.20.0\": \"1.5.12\",\n\t\"0.21.0\": \"1.5.12\",\n\t\"0.22.0\": \"1.5.12\",\n\t\"0.23.0\": \"1.5.12\",\n\t\"0.24.0\": \"1.5.12\",\n\t\"0.25.0\": \"1.5.12\",\n\t\"0.25.2\": \"1.5.12\",\n\t\"0.25.3\": \"1.5.12\",\n\t\"0.26.0\": \"1.5.12\",\n\t\"0.26.1\": \"1.5.12\",\n\t\"0.27.0\": \"1.5.12\",\n\t\"0.27.1\": \"1.5.12\",\n\t\"0.27.2\": \"1.5.12\",\n\t\"0.28.0\": \"1.5.12\",\n\t\"0.28.1\": \"1.5.12\",\n\t\"0.28.2\": \"1.5.12\",\n\t\"0.28.3\": \"1.5.12\",\n\t\"0.28.4\": \"1.5.12\",\n\t\"0.28.5\": \"1.5.12\",\n\t\"0.28.6\": \"1.5.12\",\n\t\"0.28.7\": \"1.5.12\",\n\t\"0.28.8\": \"1.5.12\",\n\t\"0.29.0\": \"1.5.12\",\n\t\"0.29.1\": \"1.5.12\"\n}\n"
  }
]