├── .npmrc ├── .eslintignore ├── .editorconfig ├── manifest.json ├── .gitignore ├── styles.css ├── tsconfig.json ├── src ├── utils │ ├── functions.ts │ └── regexes.ts ├── modals │ ├── link-verse-modal.ts │ └── copy-verse-modal.ts ├── logic │ ├── link-command.ts │ ├── common.ts │ └── copy-command.ts ├── main.ts └── settings.ts ├── version-bump.mjs ├── .eslintrc ├── package.json ├── LICENSE ├── versions.json ├── esbuild.config.mjs ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | npm node_modules 2 | build -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | insert_final_newline = true 7 | indent_style = tab 8 | indent_size = 4 9 | tab_width = 4 10 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-bible-linker", 3 | "name": "Bible Linker", 4 | "version": "1.5.15", 5 | "minAppVersion": "1.0.0", 6 | "description": "Link multiple bible verses easily", 7 | "author": "Jakub Kuchejda", 8 | "isDesktopOnly": false 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | .vs 4 | 5 | # Intellij 6 | *.iml 7 | .idea 8 | 9 | # npm 10 | node_modules 11 | 12 | # Don't include the compiled main.js file in the repo. 13 | # They should be uploaded to GitHub releases instead. 14 | main.js 15 | 16 | # Exclude sourcemaps 17 | *.map 18 | 19 | # obsidian 20 | data.json 21 | 22 | # Exclude macOS Finder (System Explorer) View States 23 | .DS_Store 24 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | 2 | /* Copy command */ 3 | .copy-preview { 4 | width: 100%; 5 | height: 15rem; 6 | opacity: 1; 7 | } 8 | 9 | 10 | /* Settings */ 11 | .important-setting .setting-item-name { 12 | font-weight: bold 13 | } 14 | 15 | .big-text-area { 16 | display: flex; 17 | flex-direction: column; 18 | } 19 | 20 | .big-text-area .setting-item-control { 21 | width: 50%; 22 | } 23 | 24 | .big-text-area textarea { 25 | margin-top: 1rem; 26 | min-height: 10rem; 27 | width: 100%; 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "isolatedModules": true, 13 | "lib": [ 14 | "DOM", 15 | "ES5", 16 | "ES6", 17 | "ES7" 18 | ] 19 | }, 20 | "include": [ 21 | "**/*.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/functions.ts: -------------------------------------------------------------------------------- 1 | const superscriptMap: Record = { 2 | "0": "⁰", 3 | "1": "¹", 4 | "2": "²", 5 | "3": "³", 6 | "4": "⁴", 7 | "5": "⁵", 8 | "6": "⁶", 9 | "7": "⁷", 10 | "8": "⁸", 11 | "9": "⁹", 12 | }; 13 | 14 | /** 15 | * Replaces all numbers in the given string with their corresponding unicode superscript version 16 | */ 17 | export function numbersToSuperscript(value: string): string { 18 | let result = ""; 19 | for (const char of value) { 20 | result += superscriptMap[char] ?? char; 21 | } 22 | return result; 23 | } 24 | 25 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-bible-linker", 3 | "version": "1.5.15", 4 | "description": "Obsidian bible linker plugin", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/node": "^16.11.6", 16 | "@typescript-eslint/eslint-plugin": "^5.2.0", 17 | "@typescript-eslint/parser": "^5.2.0", 18 | "builtin-modules": "^3.2.0", 19 | "esbuild": "^0.13.12", 20 | "obsidian": "^1.0.0", 21 | "tslib": "2.3.1", 22 | "typescript": "4.4.4" 23 | }, 24 | "dependencies": { 25 | "eslint": "^8.10.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/regexes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Regexes for verse parsing 3 | */ 4 | 5 | // Link to one verse, for example "Gen 1.1" or "Gen 1:1" 6 | export const oneVerseRegEx = new RegExp(/([^,:#]+)[,#.:;]\s*(\d+)\s*$/); 7 | 8 | // Link to multiple verses, for example "Gen 1,1-5" 9 | export const multipleVersesRegEx = new RegExp(/([^,:#]+)[,#.:;]\s*(\d+)\s*[-.=]\s*(\d+)\s*$/); 10 | 11 | // Book and chapter string 12 | export const bookAndChapterRegEx = /([^,:#]*\S)[-|\s]+(\d+)/ 13 | 14 | // Multiple chapters, for example "Gen 1-3" 15 | export const multipleChaptersRegEx = /(\d*[^\d,:#]+)\s*(\d+)\s*-\s*(\d+)\s*$/ 16 | 17 | // Can be used to determine whether given name of file is from OBSK (for example Gen-01) 18 | export const isOBSKFileRegEx = /([A-zÀ-ž0-9 ]+)-(\d{2,3})/ 19 | 20 | // Escapes given string so that it can be safely used in regular expression 21 | export function escapeForRegex(string: string) { 22 | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jakub Kuchejda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.9.7", 3 | "1.0.1": "0.12.0", 4 | "1.1.0": "0.12.0", 5 | "1.1.1": "0.12.0", 6 | "1.1.2": "0.12.0", 7 | "1.1.3": "0.12.0", 8 | "1.2.0": "0.12.0", 9 | "1.2.1": "0.12.0", 10 | "1.2.2": "0.12.0", 11 | "1.2.3": "0.12.0", 12 | "1.2.4": "0.12.0", 13 | "1.2.5": "0.12.0", 14 | "1.2.6": "0.12.0", 15 | "1.2.7": "0.12.0", 16 | "1.2.8": "0.12.0", 17 | "1.2.9": "0.12.0", 18 | "1.2.10": "0.12.0", 19 | "1.2.11": "0.12.0", 20 | "1.3.0": "0.12.0", 21 | "1.3.1": "0.12.0", 22 | "1.3.2": "0.12.0", 23 | "1.3.3": "0.12.0", 24 | "1.3.4": "0.12.0", 25 | "1.4.0": "0.12.0", 26 | "1.4.1": "0.12.0", 27 | "1.4.2": "0.12.0", 28 | "1.4.3": "0.12.0", 29 | "1.4.4": "0.12.0", 30 | "1.4.5": "0.12.0", 31 | "1.4.6": "0.12.0", 32 | "1.4.7": "0.12.0", 33 | "1.4.8": "0.12.0", 34 | "1.4.9": "0.12.0", 35 | "1.5.0": "0.12.0", 36 | "1.5.1": "0.12.0", 37 | "1.5.2": "0.12.0", 38 | "1.5.3": "0.12.0", 39 | "1.5.5": "1.0.0", 40 | "1.5.6": "1.0.0", 41 | "1.5.7": "1.0.0", 42 | "1.5.8": "1.0.0", 43 | "1.5.9": "1.0.0", 44 | "1.5.10": "1.0.0", 45 | "1.5.11": "1.0.0", 46 | "1.5.12": "1.0.0", 47 | "1.5.14": "1.0.0", 48 | "1.5.15": "1.0.0" 49 | } 50 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from 'builtin-modules' 4 | 5 | const banner = 6 | `/* 7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 8 | if you want to view the source, please visit the github repository of this plugin 9 | */ 10 | `; 11 | 12 | const prod = (process.argv[2] === 'production'); 13 | 14 | esbuild.build({ 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: ['src/main.ts'], 19 | bundle: true, 20 | external: [ 21 | 'obsidian', 22 | 'electron', 23 | '@codemirror/autocomplete', 24 | '@codemirror/closebrackets', 25 | '@codemirror/collab', 26 | '@codemirror/commands', 27 | '@codemirror/comment', 28 | '@codemirror/fold', 29 | '@codemirror/gutter', 30 | '@codemirror/highlight', 31 | '@codemirror/history', 32 | '@codemirror/language', 33 | '@codemirror/lint', 34 | '@codemirror/matchbrackets', 35 | '@codemirror/panel', 36 | '@codemirror/rangeset', 37 | '@codemirror/rectangular-selection', 38 | '@codemirror/search', 39 | '@codemirror/state', 40 | '@codemirror/stream-parser', 41 | '@codemirror/text', 42 | '@codemirror/tooltip', 43 | '@codemirror/view', 44 | ...builtins], 45 | format: 'cjs', 46 | watch: !prod, 47 | target: 'es2016', 48 | logLevel: "info", 49 | sourcemap: prod ? false : 'inline', 50 | treeShaking: true, 51 | outfile: 'main.js', 52 | }).catch(() => process.exit(1)); 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug or issue with the Bible Linker plugin 4 | title: '[BUG] ' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ## Bug Description 10 | A clear and concise description of what the bug is. 11 | 12 | ## Which Command Are You Using? 13 | 14 | - [ ] **Copy and Link Bible verses command** 15 | - [ ] **Link Bible verses command** 16 | 17 | 18 | ## Settings Review 19 | **Before submitting this issue, please confirm you have:** 20 | - [ ] Reviewed all available plugin settings in Obsidian (Settings → Bible Linker) and checked if adjusting settings resolves the issue 21 | - [ ] Verified your Bible reference format matches the plugin's expected format 22 | 23 | ## Steps to Reproduce 24 | 1. Go to '...' 25 | 2. Click on '...' 26 | 3. Enter Bible reference '...' 27 | 4. See error 28 | 29 | ## Expected Behavior 30 | A clear and concise description of what you expected to happen. 31 | 32 | ## Actual Behavior 33 | A clear and concise description of what actually happened. 34 | 35 | ## Screenshots 36 | If applicable, add screenshots to help explain your problem. 37 | 38 | ## Additional Context 39 | Add any other context about the problem here, such as: 40 | - File format of your Bible files 41 | - Specific Bible translations you're using and your vault file structure 42 | - Related settings you have enabled 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest a new feature or enhancement for the Bible Linker plugin 4 | title: '[FEATURE] ' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | ## Feature Description 10 | A clear and concise description of the feature you'd like to see added. 11 | 12 | ## Which Command Would This Apply To? 13 | 14 | - [ ] **Copy and Link Bible verses command** 15 | - [ ] **Link Bible verses command** 16 | 17 | **Note:** New features and enhancements are only being added to the **Copy and Link Bible verses command** command. If your feature request is for the **Link Bible verses command**, please consider if you could use the **Copy and Link Bible verses command** instead. 18 | 19 | ## Use Case 20 | Describe the use case for this feature. 21 | 22 | ## Proposed Solution 23 | Describe how you envision this feature working. Be as specific as possible. 24 | 25 | ## Alternative Solutions 26 | Have you considered any alternative solutions or workarounds? If so, please describe them. 27 | 28 | ## Settings Review 29 | **Before submitting this feature request, please confirm:** 30 | - [ ] I've checked all available plugin settings to ensure this isn't already possible 31 | 32 | ## Would You Be Willing to Help? 33 | - [ ] I'd be interested in helping implement this feature 34 | - [ ] I'm just suggesting the idea 35 | -------------------------------------------------------------------------------- /src/modals/link-verse-modal.ts: -------------------------------------------------------------------------------- 1 | import { App, Modal, Setting } from "obsidian"; 2 | import { PluginSettings } from "../main"; 3 | import { createLinks } from "../logic/link-command"; 4 | 5 | export enum LinkType { 6 | Basic = "Basic", 7 | Embedded = "Embedded", 8 | Invisible = "Invisible", 9 | FirstAndLast = "FirstAndLast", 10 | } 11 | 12 | /** 13 | * Modal that lets you insert bible reference by using Obsidian links 14 | */ 15 | export default class LinkVerseModal extends Modal { 16 | userInput: string; 17 | linkType: LinkType; 18 | useNewLine: boolean; 19 | onSubmit: (result: string) => void; 20 | pluginSettings: PluginSettings; 21 | 22 | handleInput = async () => { 23 | try { 24 | const res = await createLinks( 25 | this.app, 26 | this.userInput, 27 | this.linkType, 28 | this.useNewLine, 29 | this.pluginSettings 30 | ); 31 | this.close(); 32 | this.onSubmit(res); 33 | } catch (err) { 34 | return; 35 | } 36 | }; 37 | 38 | constructor( 39 | app: App, 40 | settings: PluginSettings, 41 | onSubmit: (result: string) => void 42 | ) { 43 | super(app); 44 | this.onSubmit = onSubmit; 45 | this.pluginSettings = settings; 46 | this.linkType = this.pluginSettings.linkTypePreset; 47 | this.useNewLine = this.pluginSettings.newLinePreset; 48 | } 49 | 50 | onOpen() { 51 | const { contentEl } = this; 52 | 53 | // Add heading 54 | contentEl.createEl("h3", { 55 | text: "Create Obsidian links from Bible reference", 56 | }); 57 | 58 | // Add Textbox for reference 59 | new Setting(contentEl).setName("Insert reference").addText((text) => 60 | text 61 | .onChange((value) => { 62 | this.userInput = value; 63 | }) 64 | .inputEl.focus() 65 | ); // Sets focus to input field 66 | 67 | new Setting(contentEl).setName("Link type").addDropdown((dropdown) => { 68 | dropdown.addOption(LinkType.Basic, LinkType.Basic); 69 | dropdown.addOption(LinkType.Embedded, LinkType.Embedded); 70 | dropdown.addOption(LinkType.FirstAndLast, "Show First & Last"); 71 | dropdown.addOption(LinkType.Invisible, LinkType.Invisible); 72 | dropdown.onChange((value) => (this.linkType = value as LinkType)); 73 | dropdown.setValue(this.pluginSettings.linkTypePreset); 74 | }); 75 | 76 | new Setting(contentEl) 77 | .setName("Each link on new line?") 78 | .addToggle((tgl) => { 79 | tgl.setValue(this.pluginSettings.newLinePreset); 80 | tgl.onChange((val) => { 81 | this.useNewLine = val; 82 | }); 83 | }); 84 | 85 | // Add button for submit/exit 86 | new Setting(contentEl).addButton((btn) => { 87 | btn.setButtonText("Link").setCta().onClick(this.handleInput); 88 | }); 89 | 90 | // Allow user to exit using Enter key 91 | contentEl.onkeydown = (event) => { 92 | if (event.key === "Enter") { 93 | event.preventDefault(); 94 | this.handleInput(); 95 | } 96 | }; 97 | } 98 | 99 | onClose() { 100 | const { contentEl } = this; 101 | contentEl.empty(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obsidian Bible Linker 2 | Plugin for easier linking of multiple bible verses in Obsdian.md note taking app. 3 | 4 | ## Usage 5 | 1. Use command "Copy bible verses" or "Create obsidian links to bible verses" (described bellow). 6 | 2. Insert bible link, for example "Gen 1,1-3 or Gen 1.1". Note: Links across more chapters are not supported (yet?). 7 | 3. Enjoy. 8 | 9 | ## Copy and Link Bible verses command 10 | Copies given verses from your bible files and inserts obsidian links to them. This is the main command of the plugin, with many more features than the other one. 11 | 12 | image 13 | 14 | ### Example output (input: `Gen 1,1-3`) 15 | ```md 16 | >[[Gen-01#v1|Gen 1,1-3]] In the beginning, God created the heavens and the earth. The earth was formless and empty. Darkness was on the surface of the deep and God's Spirit was hovering over the surface of the waters. God said, "Let there be light," and there was light. [[Gen-01#v1|]][[Gen-01#v2|]][[Gen-01#v3|]] 17 | ``` 18 | Note that linking is done using "invisible" links after the verses (those links are visible only in source mode) - this can be turned off in the settings, but it is not recommended if you want to use the full power of Obsidian linking. 19 | 20 | ### Pros of this approach 21 | - More verses can be displayed as one block of text, which is more visually pleasing than multiple link blocks after each other. 22 | - You can edit the text if you want (for example add some in-line notes, bold important part...) without effecting the original. 23 | 24 | ### Requirements 25 | Requires you to have bible in markdown in your vault, with similar structure to [Obsidian bible study kit](https://forum.obsidian.md/t/bible-study-in-obsidian-kit-including-the-bible-in-markdown/12503) - that is: 26 | - 1 file = 1 chapter 27 | - All verses of given chapter are present 28 | - Verse is marked with heading (any level), verse text is on the next line after said heading 29 | 30 | #### Example File 31 | ```md 32 | # Name of chapter (or some other text) 33 | 34 | ... 35 | 36 | # v1 37 | 1st verse text 38 | 39 | ###### 2 40 | 2nd verse text 41 | 42 | ### verse 3 43 | 3rd verse text 44 | ``` 45 | 46 | #### Input format 47 | - File names are deduced from the link you enter: 48 | - if your file is named "Gen 1", you will have to enter "Gen 1,1-4" 49 | - if your file is named "Genesis 1", you will have to enter "Genesis 1,1-4" 50 | - *exception*: if your file is named "Gen-01", you can type either "Gen-01,1-4" or "Gen 1,1-4" 51 | 52 | ### Multiple translation support 53 | The copy command can be used with multiple bible translations, as long as the following requirements are met: 54 | 1. Each translation must be kept in its own folder - for example Bible/NIV and Bible/KJV. 55 | 2. All translations must use the same naming conventions for files - for example if the file is named "Gen 1" in NIV, it can not be "Gn 1" in KJV. 56 | 3. The structure of the files must be roughly the same, so that they all work with the same "Verse offset" and "Verse heading level" settings. 57 | 58 | Multiple translation support must be enabled in the settings, it is off by default. 59 | 60 | ### Wrong verses are linked? Or linking doesn't work and you have files with right format? 61 | - Go to Plugin settings and try changing "Verse offset" or "Verse heading level" accordingly. 62 | 63 | ## Link Bible verses command 64 | Simpler command that only creates obsidian links based on input. You can choose if you want standard links (e.g. `[[Gen-01#v1]]`), embedded links (e.g. `![[Gen-01#v1]]`) or links invisible in the preview mode (e.g. `[[Gen-01#v1|]]`). This command is less powerful, but it also has looser requirements for your bible files, so you can use it even when the copy command does not work. 65 | 66 | image 67 | 68 | ### Requirements 69 | Basically no requirements, it just parses your input and creates links based on it. If it does not do what you want, look into the settings. 70 | 71 | ## Support 72 | If you want to support this plugin, star it on GitHub. Thank you. 73 | 74 | ## Installing 75 | Available through Obsidian Community plugins (Settings/Community plugins) 76 | 77 | ### Manual install 78 | Copy over `main.js`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/` 79 | 80 | ## Star History 81 | 82 | [![Star History Chart](https://api.star-history.com/svg?repos=kuchejak/obsidian-bible-linker-plugin&type=Date)](https://star-history.com/#kuchejak/obsidian-bible-linker-plugin&Date) 83 | -------------------------------------------------------------------------------- /src/logic/link-command.ts: -------------------------------------------------------------------------------- 1 | import {App, Notice} from "obsidian"; 2 | import {LinkType} from "../modals/link-verse-modal"; 3 | import {PluginSettings} from "../main"; 4 | import {multipleChaptersRegEx} from "../utils/regexes"; 5 | import {capitalize, getFileByFilename, parseUserBookInput, parseUserVerseInput,} from "./common"; 6 | 7 | /** 8 | * Converts biblical reference to links to given verses or books 9 | * @param app App instance 10 | * @param userInput User Input (link to verse or chapter) 11 | * @param linkType Type of link that should be used 12 | * @param useNewLine Whether or not should each link be on new line 13 | * @param settings Plugin's settings 14 | * @returns String with quote of linked verses. If converting was not successful, returns empty string. 15 | */ 16 | export async function createLinks( 17 | app: App, 18 | userInput: string, 19 | linkType: LinkType, 20 | useNewLine: boolean, 21 | settings: PluginSettings 22 | ) { 23 | if (multipleChaptersRegEx.test(userInput)) { 24 | return getLinksForChapters( 25 | app, 26 | userInput, 27 | linkType, 28 | useNewLine, 29 | settings 30 | ); 31 | } else { 32 | return getLinksForVerses( 33 | app, 34 | userInput, 35 | linkType, 36 | useNewLine, 37 | settings 38 | ); 39 | } 40 | } 41 | 42 | /** 43 | * Creates copy command output when linking multiple verses 44 | */ 45 | async function getLinksForVerses( 46 | app: App, 47 | userInput: string, 48 | linkType: LinkType, 49 | useNewLine: boolean, 50 | settings: PluginSettings 51 | ) { 52 | // eslint-disable-next-line prefer-const 53 | let { bookAndChapter, beginVerse, endVerse } = 54 | parseUserVerseInput(userInput); 55 | if (settings.shouldCapitalizeBookNames) { 56 | bookAndChapter = capitalize(bookAndChapter); // For output consistency 57 | } 58 | if (settings.verifyFilesWhenLinking) { 59 | const { fileName, tFile } = getFileByFilename(app, bookAndChapter, "/", settings); 60 | if (!tFile) { 61 | new Notice( 62 | `File "${fileName}" does not exist and verify files is set to true` 63 | ); 64 | throw `File ${fileName} does not exist, verify files = true`; 65 | } 66 | } 67 | 68 | if (beginVerse > endVerse) { 69 | new Notice("Begin verse is bigger than end verse"); 70 | throw "Begin verse is bigger than end verse"; 71 | } 72 | 73 | let res = ""; 74 | for (let i = beginVerse; i <= endVerse; i++) { 75 | const beginning = getLinkBeginning(i, beginVerse, endVerse, linkType); 76 | const ending = getLinkEnding(i, beginVerse, endVerse, linkType, bookAndChapter, settings); 77 | 78 | res += `${beginning}[[${bookAndChapter}${settings.linkSeparator}${settings.versePrefix}${i}${ending}]]`; 79 | if (useNewLine) { 80 | res += "\n"; 81 | } 82 | } 83 | return res; 84 | } 85 | 86 | function getLinkBeginning(currentVerse: number, beginVerse: number, endVerse: number, linkType: LinkType): string { 87 | switch (linkType) { 88 | case LinkType.Embedded: 89 | return "!" 90 | default: 91 | return "" 92 | } 93 | } 94 | 95 | function getLinkEnding(currentVerse: number, beginVerse: number, endVerse: number, linkType: LinkType, bookAndChapter: string, settings: PluginSettings): string { 96 | switch (linkType){ 97 | case LinkType.Invisible: 98 | return "|" 99 | case LinkType.FirstAndLast: { 100 | if (beginVerse === endVerse) { 101 | return `|${bookAndChapter}${settings.oneVerseNotation}${currentVerse}` 102 | } else if (currentVerse === beginVerse) { 103 | return `|${bookAndChapter}${settings.multipleVersesNotation}${currentVerse}` 104 | } 105 | if (currentVerse === endVerse) { 106 | return `|-${currentVerse}` 107 | } 108 | return "|"; // links between first and last verse are invisible 109 | } 110 | default: 111 | return "" 112 | } 113 | } 114 | 115 | 116 | /** 117 | * Creates copy command output when linking multiple chapters 118 | */ 119 | async function getLinksForChapters( 120 | app: App, 121 | userInput: string, 122 | linkType: LinkType, 123 | useNewLine: boolean, 124 | settings: PluginSettings 125 | ) { 126 | const { book, firstChapter, lastChapter } = parseUserBookInput(userInput); 127 | if (firstChapter > lastChapter) { 128 | new Notice("Begin chapter is bigger than end chapter"); 129 | throw "Begin chapter is bigger than end chapter"; 130 | } 131 | 132 | let res = ""; 133 | for (let i = firstChapter; i <= lastChapter; i++) { 134 | res += `[[${book} ${i}]]`; 135 | if (useNewLine) { 136 | res += "\n"; 137 | } 138 | } 139 | return res; 140 | } 141 | -------------------------------------------------------------------------------- /src/logic/common.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Capitalizes given string (skips leading whitespaces and numbers) 3 | */ 4 | import { 5 | bookAndChapterRegEx, 6 | multipleChaptersRegEx, 7 | multipleVersesRegEx, 8 | oneVerseRegEx, 9 | } from "../utils/regexes"; 10 | import { App, Notice } from "obsidian"; 11 | import { PluginSettings } from "../main"; 12 | 13 | /** 14 | * Capitalizes given string, taking leading numbers into account 15 | * @param str String that should be capitalized 16 | */ 17 | export function capitalize(str: string) { 18 | str = str.toLocaleLowerCase(); 19 | for (let i = 0; i < str.length; i++) { 20 | if (/[^\s\d.,#-]/.test(str.charAt(i))) { 21 | return ( 22 | str.slice(0, i) + str.charAt(i).toUpperCase() + str.slice(i + 1) 23 | ); 24 | } 25 | } 26 | return str; 27 | } 28 | 29 | /** 30 | * Parses input from user, expecting chapter and verses 31 | * @param userInput 32 | * @param verbose Whether or not user should be notified if the link is incorrect 33 | */ 34 | export function parseUserVerseInput(userInput: string, verbose = true) { 35 | let bookAndChapter; 36 | let beginVerse; 37 | let endVerse; 38 | 39 | switch (true) { 40 | case oneVerseRegEx.test(userInput): { 41 | // one verse 42 | const [, matchedChapter, matchedVerse] = 43 | userInput.match(oneVerseRegEx); 44 | bookAndChapter = matchedChapter; 45 | beginVerse = Number(matchedVerse); 46 | endVerse = Number(matchedVerse); 47 | break; 48 | } 49 | case multipleVersesRegEx.test(userInput): { 50 | // multiple verses, one chapter 51 | const [, matchedChapter, matchedBeginVerse, matchedEndVerse] = 52 | userInput.match(multipleVersesRegEx); 53 | bookAndChapter = matchedChapter; 54 | beginVerse = Number(matchedBeginVerse); 55 | endVerse = Number(matchedEndVerse); 56 | break; 57 | } 58 | default: { 59 | if (verbose) { 60 | new Notice(`Wrong format "${userInput}"`); 61 | } 62 | throw "Could not parse user input"; 63 | } 64 | } 65 | 66 | return { bookAndChapter, beginVerse, endVerse }; 67 | } 68 | 69 | /** 70 | * Parses input from user, expecting multiple chapters 71 | * @param userInput 72 | */ 73 | export function parseUserBookInput(userInput: string) { 74 | let book; 75 | let firstChapter; 76 | let lastChapter; 77 | 78 | switch (true) { 79 | case multipleChaptersRegEx.test(userInput): { 80 | // one verse 81 | const [, matchedBook, matchedFirstChapter, matchedLastChapter] = 82 | userInput.match(multipleChaptersRegEx); 83 | book = matchedBook.trim(); 84 | firstChapter = Number(matchedFirstChapter); 85 | lastChapter = Number(matchedLastChapter); 86 | break; 87 | } 88 | default: { 89 | new Notice(`Wrong format "${userInput}"`); 90 | throw "Could not parse user input"; 91 | } 92 | } 93 | 94 | return { book, firstChapter, lastChapter }; 95 | } 96 | 97 | /** 98 | * Tries to get tFile corresponding to given filename. If the file is not found, filename is converted to match Obsidian 99 | * Bible Study Kit naming convention and the operation is repeated. 100 | * @param app 101 | * @param filename Name of file that should be searched 102 | * @param path Path where the search should occure 103 | * @param settings Plugin settings 104 | */ 105 | export function getFileByFilename(app: App, filename: string, path: string, settings: PluginSettings) { 106 | path = path ?? "/"; 107 | let filenameCopy = filename; 108 | 109 | // Try unaltered 110 | let tFile = app.metadataCache.getFirstLinkpathDest(filenameCopy, path); 111 | if (tFile) { 112 | return { fileName: filenameCopy, tFile }; 113 | } 114 | 115 | // Try using input book mapping 116 | // eslint-disable-next-line prefer-const 117 | let [, book, chapter] = filenameCopy.match(bookAndChapterRegEx); 118 | const convertedBook = settings.inputBookMap[book.toLowerCase()] ?? book; 119 | filenameCopy = `${convertedBook} ${chapter}`; 120 | tFile = app.metadataCache.getFirstLinkpathDest(filenameCopy, path); 121 | if (tFile) { 122 | return { fileName: filenameCopy, tFile }; 123 | } 124 | 125 | // Try using "-" as separator 126 | filenameCopy = `${convertedBook}-${chapter}`; 127 | tFile = app.metadataCache.getFirstLinkpathDest(filenameCopy, path); 128 | if (tFile) { 129 | return { fileName: filenameCopy, tFile }; 130 | } 131 | 132 | // Try adding leading 0 133 | if (chapter.length == 1) { 134 | chapter = `0${chapter}`; 135 | } 136 | filenameCopy = `${convertedBook}-${chapter}`; 137 | tFile = app.metadataCache.getFirstLinkpathDest(filenameCopy, path); 138 | if (tFile) { 139 | return { fileName: filenameCopy, tFile }; 140 | } 141 | return { fileName: filename, tFile }; 142 | } 143 | -------------------------------------------------------------------------------- /src/modals/copy-verse-modal.ts: -------------------------------------------------------------------------------- 1 | import { App, ButtonComponent, Modal, Setting } from "obsidian"; 2 | import { PluginSettings } from "../main"; 3 | import {getTextOfVerses, getTranslationNameFromPath} from "../logic/copy-command"; 4 | 5 | /** 6 | * Async function for fetching preview 7 | */ 8 | async function setPreviewText( 9 | previewEl: HTMLTextAreaElement, 10 | userInput: string, 11 | pluginSettings: PluginSettings, 12 | translationPath: string, 13 | linkOnly: boolean 14 | ) { 15 | try { 16 | const res = await getTextOfVerses( 17 | this.app, 18 | userInput, 19 | pluginSettings, 20 | translationPath, 21 | linkOnly, 22 | false 23 | ); 24 | previewEl.setText(res); 25 | } catch { 26 | previewEl.setText(""); 27 | return; 28 | } 29 | } 30 | 31 | export enum LinkType { 32 | First = "First verse", 33 | FirstOtherInvis = "First verse + other invisible", 34 | FirstLast = "First and last verse", 35 | FirstLastOtherInvis = "First and last + other invisible", 36 | All = "All verses", 37 | AllInvis = "All verses, invisible", 38 | } 39 | 40 | /** 41 | * Modal that lets you insert bible reference by copying text of given verses 42 | */ 43 | export default class CopyVerseModal extends Modal { 44 | userInput: string; 45 | onSubmit: (result: string) => void; 46 | pluginSettings: PluginSettings; 47 | translationPath: string; 48 | linkOnly: boolean; 49 | 50 | handleInput = async () => { 51 | try { 52 | const res = await getTextOfVerses( 53 | this.app, 54 | this.userInput, 55 | this.pluginSettings, 56 | this.translationPath, 57 | this.linkOnly 58 | ); 59 | this.close(); 60 | this.onSubmit(res); 61 | } catch (err) { 62 | return; 63 | } 64 | }; 65 | 66 | constructor( 67 | app: App, 68 | settings: PluginSettings, 69 | onSubmit: (result: string) => void 70 | ) { 71 | super(app); 72 | this.onSubmit = onSubmit; 73 | this.pluginSettings = settings; 74 | } 75 | 76 | onOpen() { 77 | const { contentEl } = this; 78 | let previewEl: HTMLTextAreaElement; 79 | 80 | const refreshPreview = () => { 81 | setPreviewText( 82 | previewEl, 83 | this.userInput, 84 | this.pluginSettings, 85 | this.translationPath, 86 | this.linkOnly 87 | ); 88 | }; 89 | 90 | // Add heading 91 | contentEl.createEl("h3", { text: "Copy verse by bible reference" }); 92 | 93 | // Add Textbox for reference 94 | new Setting(contentEl).setName("Insert reference").addText((text) => 95 | text 96 | .onChange((value) => { 97 | this.userInput = value; 98 | refreshPreview(); 99 | }) 100 | .inputEl.focus() 101 | ); // Sets focus to input field 102 | 103 | // Add translation picker 104 | if ( 105 | this.pluginSettings.enableMultipleTranslations && 106 | this.pluginSettings.translationsPaths !== "" 107 | ) { 108 | const transationPicker = new Setting(contentEl).setName( 109 | "Pick translation" 110 | ); 111 | 112 | let buttons: ButtonComponent[] = []; 113 | let buttonPathMap = new Map(); 114 | 115 | this.pluginSettings.parsedTranslationPaths.forEach((path) => { 116 | // display translation buttons 117 | transationPicker.addButton((btn) => { 118 | buttons.push(btn); 119 | buttonPathMap.set(btn, path); 120 | btn.setButtonText(getTranslationNameFromPath(path)); 121 | }); 122 | 123 | buttons.forEach((btn) => { 124 | // make sure that only one is selected at a time 125 | btn.onClick(() => { 126 | buttons.forEach((b) => b.removeCta()); // remove CTA from all buttons 127 | btn.setCta(); // set CTA to this button 128 | this.translationPath = buttonPathMap.get(btn); 129 | refreshPreview(); 130 | }); 131 | }); 132 | 133 | // preselect the first button/trnaslation 134 | buttons.first().setCta(); 135 | this.translationPath = buttonPathMap.get(buttons.first()); 136 | }); 137 | } 138 | 139 | // add link-only options 140 | this.linkOnly = this.pluginSettings.linkOnly; 141 | new Setting(contentEl).setName("Link only").addToggle((tgl) => { 142 | tgl.setValue(this.pluginSettings.linkOnly); 143 | tgl.onChange((val) => { 144 | this.linkOnly = val; 145 | refreshPreview(); 146 | }); 147 | }); 148 | 149 | // Add preview 150 | contentEl.createEl("label", { text: "Preview" }); 151 | previewEl = contentEl.createEl("textarea", { 152 | cls: "copy-preview", 153 | attr: { readonly: true }, 154 | }); 155 | 156 | // Add button for submit/exit 157 | new Setting(contentEl).addButton((btn) => { 158 | btn.setButtonText("Link").setCta().onClick(this.handleInput); 159 | }); 160 | 161 | // Allow user to exit using Enter key 162 | contentEl.onkeydown = (event) => { 163 | if (event.key === "Enter") { 164 | event.preventDefault(); 165 | this.handleInput(); 166 | } 167 | }; 168 | } 169 | 170 | onClose() { 171 | const { contentEl } = this; 172 | contentEl.empty(); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import {Editor, Plugin} from 'obsidian'; 2 | import CopyVerseModal from 'src/modals/copy-verse-modal'; 3 | import LinkVerseModal, {LinkType} from './modals/link-verse-modal'; 4 | import {SettingsTab} from './settings'; 5 | 6 | export interface PluginSettings { 7 | // COPY 8 | // Functional 9 | verseOffset: number; 10 | verseHeadingLevel?: number; 11 | 12 | // Inserted prefixes/postfixes 13 | prefix: string; 14 | postfix: string; 15 | eachVersePrefix: string; 16 | 17 | // Links 18 | linkEndVerse: boolean; 19 | useInvisibleLinks: boolean; 20 | linkOnly: boolean; 21 | 22 | // Output format 23 | newLines: boolean; 24 | firstLinePrefix: string; 25 | insertSpace: boolean; 26 | 27 | // Notation 28 | oneVerseNotation: string; 29 | multipleVersesNotation: string; 30 | 31 | // Multiple translations 32 | enableMultipleTranslations: boolean; 33 | translationsPaths: string; 34 | parsedTranslationPaths: string[]; // callculated from translations paths, not shown to the user 35 | translationLinkingType: string; 36 | 37 | // Comments 38 | commentStart: string, 39 | commentEnd: string, 40 | 41 | // Convertors 42 | outputBookMapString: string, 43 | outputBookMap: { [key: string]: string } 44 | inputBookMapString: string, 45 | inputBookMap: { [key: string]: string } 46 | 47 | // LINK 48 | // File format 49 | linkSeparator: string; 50 | versePrefix: string; 51 | 52 | // Defaults 53 | linkTypePreset: LinkType; 54 | newLinePreset: boolean; 55 | 56 | // Format 57 | shouldCapitalizeBookNames: boolean; 58 | 59 | // Misc 60 | verifyFilesWhenLinking: boolean; 61 | } 62 | 63 | const DEFAULT_SETTINGS: Partial = { 64 | // COPY 65 | // Functional 66 | verseOffset: 0, 67 | verseHeadingLevel: undefined, 68 | 69 | // Inserted prefixes/postfixes 70 | prefix: "", 71 | postfix: "", 72 | eachVersePrefix: "", 73 | 74 | // Links 75 | linkEndVerse: false, 76 | useInvisibleLinks: true, 77 | linkOnly: false, 78 | 79 | // Output format 80 | newLines: false, 81 | firstLinePrefix: "", 82 | insertSpace: true, 83 | 84 | // Notation 85 | oneVerseNotation: ".", 86 | multipleVersesNotation: ",", 87 | 88 | // Multiple translations 89 | enableMultipleTranslations: false, 90 | translationsPaths: "", 91 | parsedTranslationPaths: [], 92 | translationLinkingType: "all", 93 | 94 | // Comments 95 | commentStart: "", 96 | commentEnd: "", 97 | 98 | // Convertors 99 | outputBookMapString: "", 100 | outputBookMap: {}, 101 | inputBookMapString: "", 102 | inputBookMap: {}, 103 | 104 | // LINK 105 | // File format 106 | linkSeparator: "#", 107 | versePrefix: "", 108 | 109 | // Defaults 110 | linkTypePreset: LinkType.Basic, 111 | newLinePreset: true, 112 | 113 | // Format 114 | shouldCapitalizeBookNames: true, 115 | 116 | // Misc 117 | verifyFilesWhenLinking: false, 118 | }; 119 | 120 | 121 | function replaceRangeAndMoveCursor(str: string, editor: Editor) { 122 | editor.replaceRange(str, editor.getCursor()); 123 | let offset = editor.posToOffset(editor.getCursor()) 124 | offset += str.length; 125 | editor.setCursor(editor.offsetToPos(offset)); 126 | } 127 | 128 | export default class BibleLinkerPlugin extends Plugin { 129 | settings: PluginSettings; 130 | 131 | // Opens modal for text copying 132 | openCopyModal = () => { 133 | const editor = this.app.workspace.activeEditor?.editor 134 | if (editor) { 135 | new CopyVerseModal(this.app, this.settings, 136 | (str) => replaceRangeAndMoveCursor(str, editor) 137 | ).open(); 138 | } 139 | } 140 | 141 | // Opens modal for creating obsidian links 142 | openObsidianLinkModal = () => { 143 | const editor = this.app.workspace.activeEditor?.editor 144 | if (editor) { 145 | new LinkVerseModal(this.app, this.settings, 146 | (str) => replaceRangeAndMoveCursor(str, editor) 147 | ).open(); 148 | } 149 | } 150 | 151 | 152 | async loadSettings() { 153 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()) 154 | } 155 | 156 | async saveSettings() { 157 | await this.saveData(this.settings) 158 | } 159 | 160 | // Run once when plugin is loaded 161 | async onload() { 162 | // Handle settings 163 | await this.loadSettings(); 164 | this.addSettingTab(new SettingsTab(this.app, this)) 165 | 166 | // Add icon to insert link 167 | // this.addRibbonIcon("link", "Insert bible link", (evt: MouseEvent) => this.openCopyModal()); 168 | 169 | // Command to insert link (only available in editor mode) 170 | this.addCommand({ 171 | id: 'insert-bible-link', // ID left to preserve user's key mappings 172 | name: "Copy and Link Bible verses", 173 | icon: "copy", 174 | editorCallback: this.openCopyModal 175 | }) 176 | 177 | // Command to insert link (only available in editor mode) 178 | this.addCommand({ 179 | id: 'insert-bible-link-obsidian-link', 180 | name: "Link Bible verses", 181 | icon: "link", 182 | editorCallback: this.openObsidianLinkModal 183 | }) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/logic/copy-command.ts: -------------------------------------------------------------------------------- 1 | import { App, HeadingCache, Notice, TFile } from "obsidian"; 2 | import { PluginSettings } from "../main"; 3 | import {bookAndChapterRegEx, escapeForRegex, isOBSKFileRegEx} from "../utils/regexes"; 4 | import { capitalize, getFileByFilename as getTFileByFilename, parseUserVerseInput } from "./common"; 5 | import {numbersToSuperscript} from "../utils/functions"; 6 | 7 | /** 8 | * Converts biblical reference to text of given verses 9 | * @param app App instance 10 | * @param userInput User Input (link to verse) 11 | * @param settings Plugin settings 12 | * @param translationPath Path to translation that should be used 13 | * @param linkOnly Whether to insert output only link or also include text 14 | * @param verbose Whether or not user should be notified if the link is incorrect 15 | * @returns String with quote of linked verses. If converting was not successful, returns empty string. 16 | * @verbose Determines if Notices will be shown or not 17 | */ 18 | export async function getTextOfVerses(app: App, userInput: string, settings: PluginSettings, translationPath: string, linkOnly: boolean, verbose = true): Promise { 19 | 20 | // eslint-disable-next-line prefer-const 21 | let { bookAndChapter, beginVerse, endVerse } = parseUserVerseInput(userInput, verbose); 22 | bookAndChapter = capitalize(bookAndChapter) // For output consistency 23 | const { fileName, tFile } = getTFileByFilename(app, bookAndChapter, translationPath, settings); 24 | if (tFile) { 25 | return await createCopyOutput(app, tFile, fileName, beginVerse, endVerse, settings, translationPath, linkOnly, verbose); 26 | } else { 27 | if (verbose) { 28 | new Notice(`File ${bookAndChapter} not found`); 29 | } 30 | throw "File not found" 31 | } 32 | } 33 | 34 | /** 35 | * Returns text of given verse using given headings and lines. 36 | * @param verseNumber Number of desired verse. 37 | * @param headings List of headings that should be searched. Second heading must correspond to first verse, third heading to second verse and so on. 38 | * @param lines Lines of file from which verse text should be taken. 39 | * @param keepNewlines If set to true, text will contain newlines if present in source, if set to false, newlines will be changed to spaces 40 | * @param newLinePrefix Prefix for each line of verse, if verse is multiline and keepNewLines = true 41 | * @returns Text of given verse. 42 | */ 43 | function getVerseText(verseNumber: number, headings: HeadingCache[], lines: string[], keepNewlines: boolean, newLinePrefix: string) { 44 | if (verseNumber >= headings.length) { // out of range 45 | new Notice("Verse out of range for given file") 46 | throw `VerseNumber ${verseNumber} is out of range of headings with length ${headings.length}` 47 | } 48 | 49 | const headingLine = headings[verseNumber].position.start.line; 50 | if (headingLine + 1 >= lines.length) { // out of range 51 | new Notice("Logical error - please create issue on plugin's GitHub with your input and the file you were referencing. Thank you!") 52 | throw `HeadingLine ${headingLine + 1} is out of range of lines with length ${lines}` 53 | } 54 | 55 | // This part is necessary for verses that span over multiple lines 56 | let output = ""; 57 | let line = ""; 58 | let i = 1; 59 | let isFirst = true; 60 | 61 | // eslint-disable-next-line no-constant-condition 62 | while (true) { 63 | line = lines[headingLine + i]; // get next line 64 | if (/^#/.test(line) || (!line && !isFirst)) { 65 | break; // heading line (next verse) or empty line after verse => do not continue 66 | } 67 | i++; 68 | if (line) { // if line has content (is not empty string) 69 | if (!isFirst) { // If it is not first line of the verse, add divider 70 | output += keepNewlines ? `\n${newLinePrefix}` : " "; 71 | } 72 | isFirst = false; 73 | output += line; 74 | } 75 | } 76 | return output; 77 | } 78 | 79 | /** 80 | * Replaces "\n" with newline character in given string (when user inputs "\n" in the settings it is automatically converted to "\\n" and does not work as newline) 81 | */ 82 | function replacePlaceholdersInPostfix(input: string, translationPath: string) { 83 | let result = input.replace(/\\n/g, "\n",); 84 | if (translationPath != "" && translationPath != undefined) { 85 | result = result.replace(/{t}/g, getTranslationNameFromPath(translationPath)); 86 | } 87 | return result; 88 | } 89 | 90 | /** 91 | * Returns the name of the translation from the path to it. 92 | * For example for path "personal/bible/NIV/" it will return "NIV" 93 | * @param path 94 | */ 95 | export function getTranslationNameFromPath(path: string) { 96 | const splitPath = path.split("/"); 97 | return splitPath[splitPath.length - 2]; 98 | } 99 | 100 | /** 101 | * Replaces the given book with its display value defined in the settings. If no mapping exists, the original value is returned. 102 | * @param book Book that should be replaced 103 | * @param settings Plugin's settings 104 | */ 105 | function getDisplayBookName(book: string, settings: PluginSettings) { 106 | return settings.outputBookMap[book.toLowerCase()] ?? book; 107 | } 108 | 109 | /** 110 | * Takes orginal filename and converts it to human-readable version if Bible study kit is used (removes "-" and leading zeros) 111 | */ 112 | function createBookAndChapterOutput(fileBasename: string, settings: PluginSettings) { 113 | const isOBSK = isOBSKFileRegEx.test(fileBasename); 114 | const regex = isOBSK ? isOBSKFileRegEx : bookAndChapterRegEx; 115 | 116 | // eslint-disable-next-line prefer-const 117 | let [, book, chapter] = fileBasename.match(regex); 118 | if (isOBSK && chapter.toString()[0] === "0") { // remove leading zeros in OBSK chapters (eg. Gen-01) 119 | chapter = chapter.substring(1); 120 | } 121 | return getDisplayBookName(book, settings) + " " + chapter; 122 | } 123 | 124 | /** 125 | * Returns path to folder in which given file is located for main translation 126 | */ 127 | function getFileFolderInTranslation(app: App, filename: string, translation: string, settings: PluginSettings) { 128 | const tFileInfo = getTFileByFilename(app, filename, translation, settings); 129 | return tFileInfo.tFile.parent.path; 130 | } 131 | 132 | async function createCopyOutput(app: App, tFile: TFile, fileName: string, beginVerse: number, endVerse: number, settings: PluginSettings, translationPath: string, linkOnly: boolean, verbose: boolean) { 133 | const bookAndChapterOutput = createBookAndChapterOutput(tFile.basename, settings); 134 | const file = app.vault.read(tFile) 135 | const lines = (await file).split(/\r?\n/) 136 | const verseHeadingLevel = settings.verseHeadingLevel 137 | const headings = app.metadataCache.getFileCache(tFile).headings.filter(heading => !verseHeadingLevel || heading.level === verseHeadingLevel) 138 | const beginVerseNoOffset = beginVerse 139 | beginVerse += settings.verseOffset 140 | endVerse += settings.verseOffset 141 | const nrOfVerses = headings.length - 1; 142 | const maxVerse = endVerse < nrOfVerses ? endVerse : nrOfVerses; // if endverse is bigger than chapter allows, it is lowered to maximum 143 | const maxVerseNoOffset = maxVerse - settings.verseOffset; 144 | 145 | 146 | if (beginVerse > maxVerse) { 147 | if (verbose) { 148 | new Notice("Begin verse is bigger than end verse or chapter maximum") 149 | } 150 | throw "Begin verse is bigger than end verse or chapter maximum" 151 | } 152 | 153 | 154 | // 1 - Link to verses 155 | let postfix = "", res = "", pathToUse = ""; 156 | if (!linkOnly) { 157 | res = settings.prefix; 158 | postfix = settings.postfix ? replacePlaceholdersInPostfix(settings.postfix, translationPath) : " "; 159 | } 160 | if (settings.enableMultipleTranslations) { 161 | if (settings.translationLinkingType !== "main") // link the translation that is currently being used 162 | pathToUse = getFileFolderInTranslation(app, fileName, translationPath, settings); 163 | else { // link main translation 164 | pathToUse = getFileFolderInTranslation(app, fileName, settings.parsedTranslationPaths.first(), settings); 165 | } 166 | } 167 | 168 | if (settings.newLines && !linkOnly) { 169 | res += `${settings.firstLinePrefix}` 170 | } 171 | 172 | if (beginVerse === maxVerse) { 173 | res += `[[${pathToUse ? pathToUse + "/" : ""}${fileName}#${headings[beginVerse].heading}|${bookAndChapterOutput}${settings.oneVerseNotation}${beginVerseNoOffset}]]${postfix}` // [[Gen 1#1|Gen 1,1.1]] 174 | } else if (settings.linkEndVerse) { 175 | res += `[[${pathToUse ? pathToUse + "/" : ""}${fileName}#${headings[beginVerse].heading}|${bookAndChapterOutput}${settings.multipleVersesNotation}${beginVerseNoOffset}-]]` // [[Gen 1#1|Gen 1,1-]] 176 | res += `[[${pathToUse ? pathToUse + "/" : ""}${fileName}#${headings[maxVerse].heading}|${maxVerseNoOffset}]]${postfix}`; // [[Gen 1#3|3]] 177 | } else { 178 | res += `[[${pathToUse ? pathToUse + "/" : ""}${fileName}#${headings[beginVerse].heading}|${bookAndChapterOutput}${settings.multipleVersesNotation}${beginVerseNoOffset}-${maxVerseNoOffset}]]${postfix}` // [[Gen 1#1|Gen 1,1-3]] 179 | } 180 | 181 | // 2 - Text of verses 182 | if (!linkOnly) { 183 | for (let i = beginVerse; i <= maxVerse; i++) { 184 | let versePrefix = ""; 185 | const versePostfix = settings.insertSpace ? " " : ""; 186 | if (settings.eachVersePrefix) { 187 | versePrefix += settings.eachVersePrefix.replace(/{n}/g, `${i - settings.verseOffset}`); 188 | versePrefix = versePrefix.replace(/{u}/g, numbersToSuperscript(`${i - settings.verseOffset}`)); 189 | versePrefix = versePrefix.replace(/{f}/g, `${fileName}`); 190 | if (settings.enableMultipleTranslations) { 191 | versePrefix = versePrefix.replace(/{t}/g, `${getTranslationNameFromPath(translationPath)}`); 192 | } 193 | } 194 | let verseText = getVerseText(i, headings, lines, settings.newLines, settings.prefix); 195 | 196 | if (settings.commentStart !== "" && settings.commentEnd !== "") { 197 | const escapedStart = escapeForRegex(settings.commentStart); 198 | const escapedEnd = escapeForRegex(settings.commentEnd); 199 | const replaceRegex = new RegExp(`${escapedStart}.*?${escapedEnd}`, 'gs'); 200 | verseText = verseText.replace(replaceRegex, ''); 201 | } 202 | if (settings.newLines) { 203 | res += "\n" + settings.prefix + versePrefix + verseText; 204 | } else { 205 | res += versePrefix + verseText + versePostfix; 206 | } 207 | } 208 | } 209 | 210 | // 3 - Invisible links 211 | if (!settings.useInvisibleLinks) return res; 212 | if ((beginVerse == maxVerse || (settings.linkEndVerse && beginVerse == maxVerse - 1)) // No need to add another link, when only one verse is being linked 213 | && (!settings.enableMultipleTranslations 214 | || settings.translationLinkingType === "main" 215 | || settings.translationLinkingType === "used" 216 | || (settings.translationLinkingType === "usedAndMain" && translationPath === settings.parsedTranslationPaths.first()))) 217 | return res; 218 | 219 | if (settings.newLines) { 220 | res += `\n${settings.prefix}`; 221 | } 222 | const lastVerseToLink = settings.linkEndVerse ? maxVerse - 1 : maxVerse; 223 | for (let i = beginVerse; i <= lastVerseToLink; i++) { // beginVerse + 1 because link to first verse is already inserted before the text 224 | if (!settings.enableMultipleTranslations) { 225 | if (i == beginVerse) continue; // already linked in the first link before text 226 | res += `[[${fileName}#${headings[i].heading}|]]` 227 | } 228 | else { // multiple translations 229 | let translationPathsToUse: string[] = []; 230 | switch (settings.translationLinkingType) { 231 | case "all": 232 | translationPathsToUse = settings.parsedTranslationPaths.map((tr) => getFileFolderInTranslation(app, fileName, tr, settings)) 233 | break; 234 | case "used": 235 | if (i == beginVerse) continue; // already linked in the first link before text 236 | translationPathsToUse = [getFileFolderInTranslation(app, fileName, translationPath, settings)] 237 | break; 238 | case "usedAndMain": 239 | if (translationPath !== settings.parsedTranslationPaths.first()) { 240 | translationPathsToUse = [getFileFolderInTranslation(app, fileName, translationPath, settings), 241 | getFileFolderInTranslation(app, fileName, settings.parsedTranslationPaths.first(), settings)]; 242 | } 243 | else { 244 | if (i == beginVerse) continue; // already linked in the first link before text 245 | translationPathsToUse = [getFileFolderInTranslation(app, fileName, translationPath, settings)]; 246 | } 247 | break; 248 | case "main": 249 | if (i == beginVerse) continue; // already linked in the first link before text 250 | translationPathsToUse = [getFileFolderInTranslation(app, fileName, settings.parsedTranslationPaths.first(), settings)]; 251 | break; 252 | default: 253 | break; 254 | } 255 | if (translationPathsToUse.length === 0) return; 256 | 257 | translationPathsToUse.forEach((translationPath) => { 258 | res += `[[${translationPath}/${fileName}#${headings[i].heading}|]]` 259 | }) 260 | } 261 | 262 | } 263 | return res; 264 | } 265 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import {App, Notice, PluginSettingTab, Setting} from "obsidian"; 2 | import BibleLinkerPlugin from "./main"; 3 | import {LinkType} from "./modals/link-verse-modal"; 4 | 5 | /** 6 | * Settings for plugin 7 | */ 8 | export class SettingsTab extends PluginSettingTab { 9 | plugin: BibleLinkerPlugin; 10 | 11 | constructor(app: App, plugin: BibleLinkerPlugin) { 12 | super(app, plugin); 13 | this.plugin = plugin; 14 | } 15 | 16 | display() { 17 | const { containerEl } = this; 18 | 19 | containerEl.empty(); 20 | 21 | containerEl.createEl("h1", { text: "Copy and Link Bible verses command" }); 22 | containerEl.createEl("h4", { text: "Functional" }); 23 | 24 | new Setting(containerEl) 25 | .setName("Verse offset") 26 | .setDesc('Change this if wrong verses are being linked, e.g. you want "Gen 1,1-3" but output is text from verses 2-4 → set this to -1') 27 | .setClass("important-setting") 28 | .addText((inputBox) => 29 | inputBox 30 | .setValue(this.plugin.settings.verseOffset.toString()) 31 | .onChange(async (value) => { 32 | const number = Number.parseInt(value); 33 | if (value === "-") return; 34 | if (Number.isNaN(number)) { 35 | new Notice("Invalid input, please insert valid integer"); 36 | inputBox.setValue(""); 37 | return; 38 | } 39 | this.plugin.settings.verseOffset = number; 40 | await this.plugin.saveSettings(); 41 | }) 42 | ) 43 | 44 | new Setting(containerEl) 45 | .setName("Verse heading level") 46 | .setDesc('If set, only headings of specified level are considered verses (if first heading of this level is always a verse, also set "Verse offset" to -1)') 47 | .addDropdown((dropdown) => { 48 | dropdown.addOption("any", "any") 49 | dropdown.addOption("6", "######") 50 | dropdown.addOption("5", "#####") 51 | dropdown.addOption("4", "####") 52 | dropdown.addOption("3", "###") 53 | dropdown.addOption("2", "##") 54 | dropdown.addOption("1", "#") 55 | dropdown.setValue(this.plugin.settings.verseHeadingLevel?.toString() ?? "any") 56 | dropdown.onChange(async (value) => { 57 | this.plugin.settings.verseHeadingLevel = value === "any" ? undefined : Number(value); 58 | await this.plugin.saveSettings(); 59 | }) 60 | }) 61 | 62 | containerEl.createEl("h4", { text: "Inserted prefixes/postfixes" }); 63 | 64 | new Setting(containerEl) 65 | .setName("Line prefix") 66 | .setDesc("String inserted in front of every line, for example '>' for quote. Note: If you set 'Put each verse on a new line?' to true, the prefix will be inserted in front of every line.") 67 | .setClass("important-setting") 68 | .addText((inputBox) => 69 | inputBox 70 | .setPlaceholder("Insert prefix here") 71 | .setValue(this.plugin.settings.prefix) 72 | .onChange(async (value) => { 73 | this.plugin.settings.prefix = value; 74 | await this.plugin.saveSettings(); 75 | }) 76 | ) 77 | 78 | new Setting(containerEl) 79 | .setName("Link postfix") 80 | .setDesc("String inserted after biblical link, you can use \\n to insert newline. If you are using multiple translations \"{t}\" will insert the name of the one used.") 81 | .addText((inputBox) => 82 | inputBox 83 | .setPlaceholder("Insert postfix here") 84 | .setValue(this.plugin.settings.postfix) 85 | .onChange(async (value) => { 86 | this.plugin.settings.postfix = value; 87 | await this.plugin.saveSettings(); 88 | }) 89 | ) 90 | 91 | new Setting(containerEl) 92 | .setName("Each verse prefix") 93 | .setDesc("String inserted in front of every copied verse. You can use \"{n}\" where you want number of given verse inserted, for example \"**{n}** \" will make each verse start with bold verse number. You can also use \"{f}\" to insert name of the corresponding file (for example to create obsidian links). If you are using multiple translations \"{t}\" will insert the name of the one used. \"{u}\" will insert the number of the verse as unicode superscript. Leave empty for no prefix.") 94 | .addText((inputBox) => 95 | inputBox 96 | .setPlaceholder("Insert prefix here") 97 | .setValue(this.plugin.settings.eachVersePrefix) 98 | .onChange(async (value) => { 99 | this.plugin.settings.eachVersePrefix = value; 100 | await this.plugin.saveSettings(); 101 | }) 102 | ) 103 | 104 | 105 | containerEl.createEl("h4", { text: "Links" }); 106 | 107 | new Setting(containerEl) 108 | .setName("Link to last verse?") 109 | .setDesc("Should last verse be linked in the visible link before text of verses?") 110 | .addToggle((toggle) => 111 | toggle 112 | .setValue(this.plugin.settings.linkEndVerse) 113 | .onChange(async (value) => { 114 | this.plugin.settings.linkEndVerse = value; 115 | await this.plugin.saveSettings(); 116 | }) 117 | ) 118 | 119 | new Setting(containerEl) 120 | .setName("Add invisible links?") 121 | .setDesc("Invisible links are added to each verse used (so you can find the connections later), they are only visible in source mode.") 122 | .addToggle((toggle) => 123 | toggle 124 | .setValue(this.plugin.settings.useInvisibleLinks) 125 | .onChange(async (value) => { 126 | this.plugin.settings.useInvisibleLinks = value; 127 | await this.plugin.saveSettings(); 128 | }) 129 | ) 130 | 131 | new Setting(containerEl) 132 | .setName("Link only default") 133 | .setDesc("What the link only option should be set to by default") 134 | .addToggle((toggle) => 135 | toggle 136 | .setValue(this.plugin.settings.linkOnly) 137 | .onChange(async (value) => { 138 | this.plugin.settings.linkOnly = value; 139 | await this.plugin.saveSettings(); 140 | }) 141 | ) 142 | 143 | 144 | containerEl.createEl("h4", { text: "Output format" }); 145 | 146 | new Setting(containerEl) 147 | .setName("Put each verse on a new line?") 148 | .setClass("important-setting") 149 | .setDesc("Each verse is inserted on a new line (with Link prefix).") 150 | .addToggle((toggle) => 151 | toggle 152 | .setValue(this.plugin.settings.newLines) 153 | .onChange(async (value) => { 154 | this.plugin.settings.newLines = value; 155 | await this.plugin.saveSettings(); 156 | this.display(); 157 | }) 158 | ) 159 | 160 | if (this.plugin.settings.newLines) { 161 | new Setting(containerEl) 162 | .setName("First line prefix") 163 | .setDesc("Special prefix that will be inserted in front of the first line only, right after the \"Line prefix\". Handy for callouts. (Only applied when Put each verse on a new line? is set to true)") 164 | .addText((inputBox) => 165 | inputBox 166 | .setPlaceholder("First line prefix") 167 | .setValue(this.plugin.settings.firstLinePrefix) 168 | .onChange(async (value) => { 169 | this.plugin.settings.firstLinePrefix = value; 170 | await this.plugin.saveSettings(); 171 | }) 172 | ) 173 | } 174 | else { 175 | new Setting(containerEl) 176 | .setName("Insert space between verses?") 177 | .setDesc("Should space be inserted between verses? (Only applied when Put each verse on a new line? is set to false. Useful for languages such as Chinese.)") 178 | .setDisabled(!this.plugin.settings.newLines) 179 | .addToggle((toggle) => 180 | toggle 181 | .setValue(this.plugin.settings.insertSpace) 182 | .onChange(async (value) => { 183 | this.plugin.settings.insertSpace = value; 184 | await this.plugin.saveSettings(); 185 | }) 186 | ) 187 | } 188 | 189 | 190 | 191 | containerEl.createEl("h4", { text: "Notation" }); 192 | containerEl.createEl("p", { text: "Also used in the link command when the \"Show First & Last\" link type is used." }); 193 | 194 | new Setting(containerEl) 195 | .setName("One verse notation") 196 | .setDesc("This is the symbol that will be used between chapter number and verse number when copying one verse. For example \".\" → Gen 1.1." ) 197 | .addText((inputBox) => 198 | inputBox 199 | .setPlaceholder("Insert notation symbol here") 200 | .setValue(this.plugin.settings.oneVerseNotation) 201 | .onChange(async (value) => { 202 | this.plugin.settings.oneVerseNotation = value; 203 | await this.plugin.saveSettings(); 204 | }) 205 | ) 206 | 207 | new Setting(containerEl) 208 | .setName("Multiple verses notation") 209 | .setDesc("This is the symbol that will be used between chapter number and verse number when copying multiple verses. For example \",\" → Gen 1,1-3.") 210 | .addText((inputBox) => 211 | inputBox 212 | .setPlaceholder("Insert notation symbol here") 213 | .setValue(this.plugin.settings.multipleVersesNotation) 214 | .onChange(async (value) => { 215 | this.plugin.settings.multipleVersesNotation = value; 216 | await this.plugin.saveSettings(); 217 | }) 218 | ) 219 | 220 | containerEl.createEl("h4", { text: "Multiple translations" }); 221 | 222 | new Setting(containerEl) 223 | .setName("Enable multiple translations") 224 | .addToggle((toggle) => 225 | toggle 226 | .setValue(this.plugin.settings.enableMultipleTranslations) 227 | .onChange(async (value) => { 228 | this.plugin.settings.enableMultipleTranslations = value; 229 | await this.plugin.saveSettings(); 230 | this.display(); 231 | }) 232 | ) 233 | 234 | 235 | if (this.plugin.settings.enableMultipleTranslations) { 236 | new Setting(containerEl) 237 | .setName("Paths to translations with their names") 238 | .setDesc("Input full paths from the root vault folder to folders containing Bible translations, each translation on separate line. An example of one entry: \"Bible/NIV/\". The plugin will search for corresponding Bible files using given paths as starting points. Make sure there are no duplicate files in given paths, otherwise it is hard to tell what the output will be. The first translation will be considered your main translation.").addTextArea((inputBox) => 239 | inputBox 240 | .setPlaceholder("Bible/NIV/\nBible/ESV/") 241 | .setValue(this.plugin.settings.translationsPaths) 242 | .onChange(async (value) => { 243 | const inputPaths = value.split(/\r?\n|\r/); // split user input by lines (regex takes into account all possible line endings) 244 | const paths: string[] = []; 245 | inputPaths.forEach((path) => { // parse user input for later use 246 | if (path.at(-1) !== "/") { // Add potentially missing '/' to path 247 | paths.push(path + "/"); 248 | } 249 | else { 250 | paths.push(path) 251 | } 252 | }) 253 | this.plugin.settings.translationsPaths = value; 254 | this.plugin.settings.parsedTranslationPaths = paths; 255 | await this.plugin.saveSettings(); 256 | }) 257 | ) 258 | 259 | 260 | new Setting(containerEl) 261 | .setName("What to link") 262 | .setDesc("Choose what translations should be linked when copying a verse.") 263 | .addDropdown((dropdown) => { 264 | dropdown.addOption("all", "Link to all translations") 265 | dropdown.addOption("used", "Link only to used translation") 266 | dropdown.addOption("usedAndMain", "Link to used and main translation") 267 | dropdown.addOption("main", "Link only to main translation") 268 | dropdown.setValue(this.plugin.settings.translationLinkingType) 269 | dropdown.onChange(async (value) => { 270 | this.plugin.settings.translationLinkingType = value; 271 | await this.plugin.saveSettings(); 272 | }) 273 | }) 274 | } 275 | 276 | containerEl.createEl("h4", { text: "Comments" }); 277 | containerEl.createEl("p", { text: "Use this if you have comments right in the Biblical text that you want to ignore when copying verses." }); 278 | new Setting(containerEl) 279 | .setName("Comment beginning") 280 | .setDesc("String that is used to mark the beginning of a comment, won't be used if it is set to an empty string.") 281 | .addText((inputBox) => 282 | inputBox 283 | .setPlaceholder("/*") 284 | .setValue(this.plugin.settings.commentStart) 285 | .onChange(async (value) => { 286 | this.plugin.settings.commentStart = value; 287 | await this.plugin.saveSettings(); 288 | }) 289 | ) 290 | 291 | new Setting(containerEl) 292 | .setName("Comment ending") 293 | .setDesc("String that is used to mark the end of a comment, won't be used if it is set to an empty string.") 294 | .addText((inputBox) => 295 | inputBox 296 | .setPlaceholder("*/") 297 | .setValue(this.plugin.settings.commentEnd) 298 | .onChange(async (value) => { 299 | this.plugin.settings.commentEnd = value; 300 | await this.plugin.saveSettings(); 301 | }) 302 | ) 303 | 304 | containerEl.createEl("h4", { text: "Convertors" }); 305 | function parseStringToDictionary(input: string): { [key: string]: string } { 306 | const dictionary: { [key: string]: string } = {}; 307 | 308 | // Normalize the line endings to \n 309 | const normalizedInput = input.replace(/\r\n|\r/g, '\n'); 310 | 311 | // Split the input string by line breaks 312 | const lines = normalizedInput.split('\n'); 313 | 314 | // Process each line to fill the dictionary 315 | lines.forEach(line => { 316 | // Check if the line contains a colon 317 | if (line.includes(':')) { 318 | const [key, value] = line.split(':'); 319 | if (key && value) { 320 | dictionary[key.toLowerCase()] = value; 321 | } 322 | } 323 | }); 324 | 325 | return dictionary; 326 | } 327 | 328 | new Setting(containerEl) 329 | .setName("Output book name convertor") 330 | .setDesc("You can specify conversions that will be applied to the visible book name alias. For example, if you put in \"3J:3 John\", the output will be changed from \"[[3 John-01#v1|3J 1.1]]\" to \"[[3 John-01#v1|3 John 1.1]]\". The format used is \"From:To\", each entry on it's own line. TIP: ChatGPT (or similar AI tool) will probably be able to help you when creating the input.") 331 | .setClass("big-text-area") 332 | .addTextArea((inputBox) => 333 | inputBox 334 | .setPlaceholder("Gn:Genesis\nEx:Exodus\n...") 335 | .setValue(this.plugin.settings.outputBookMapString) 336 | .onChange(async (value) => { 337 | this.plugin.settings.outputBookMapString = value; 338 | this.plugin.settings.outputBookMap = parseStringToDictionary(value); 339 | await this.plugin.saveSettings(); 340 | }) 341 | ) 342 | 343 | new Setting(containerEl) 344 | .setName("Input book name convertor") 345 | .setDesc("You can specify conversions that will be applied to the used book name when searching for text of a verse. For example, if you put in \"Gn:Gen\", the input \"Gn 1,1\" will work even when the file is called \"Gen 1,1\". The format used is again \"From:To\", each entry on it's own line, and will be used by the plugin when the search fails using the unchanged input. Multiple entries can have same result mapping, for example you can use \"G:Gen\" and \"Gn:Gen\".") 346 | .setClass("big-text-area") 347 | .addTextArea((inputBox) => 348 | inputBox 349 | .setPlaceholder("G:Gen\nGn:Gen\nL:Lk\n...") 350 | .setValue(this.plugin.settings.inputBookMapString) 351 | .onChange(async (value) => { 352 | this.plugin.settings.inputBookMapString = value; 353 | this.plugin.settings.inputBookMap = parseStringToDictionary(value); 354 | await this.plugin.saveSettings(); 355 | }) 356 | ) 357 | // LINK ------------------------------------------------------------------------------------------------------------- 358 | 359 | containerEl.createEl("h1", { text: "Link Bible verses command" }); 360 | 361 | containerEl.createEl("h4", { text: "File format" }); 362 | new Setting(containerEl) 363 | .setName("Link separator") 364 | .setDesc("This is the separator that will be used when linking, e.g. if you enter '#' here, output will be [[Gen 1#1]]. If you are using headings to mark verses, use '#'. If you are using block references, use '^'.") 365 | .setClass("important-setting") 366 | .addText((inputBox) => 367 | inputBox 368 | .setPlaceholder("Insert separator here") 369 | .setValue(this.plugin.settings.linkSeparator) 370 | .onChange(async (value) => { 371 | this.plugin.settings.linkSeparator = value; 372 | await this.plugin.saveSettings(); 373 | }) 374 | ) 375 | 376 | 377 | new Setting(containerEl) 378 | .setName("Verse prefix") 379 | .setDesc('Fill this if you are using verse prefixes in your bible files, e.g. you have "v1" in your file → set to "v".') 380 | .setClass("important-setting") 381 | .addText((inputBox) => 382 | inputBox 383 | .setPlaceholder("Insert prefix here") 384 | .setValue(this.plugin.settings.versePrefix) 385 | .onChange(async (value) => { 386 | this.plugin.settings.versePrefix = value; 387 | await this.plugin.saveSettings(); 388 | }) 389 | ) 390 | 391 | 392 | containerEl.createEl("h4", { text: "Defaults" }); 393 | 394 | new Setting(containerEl) 395 | .setName("Link type default value") 396 | .setDesc("Value that will be selected by default in link modal.") 397 | .addDropdown((dropdown) => { 398 | dropdown.addOption(LinkType.Basic, LinkType.Basic) 399 | dropdown.addOption(LinkType.Embedded, LinkType.Embedded) 400 | dropdown.addOption(LinkType.FirstAndLast, "Show First & Last"); 401 | dropdown.addOption(LinkType.Invisible, LinkType.Invisible) 402 | dropdown.setValue(this.plugin.settings.linkTypePreset) 403 | dropdown.onChange(async (value) => { 404 | this.plugin.settings.linkTypePreset = value as LinkType; 405 | await this.plugin.saveSettings(); 406 | }) 407 | }) 408 | 409 | new Setting(containerEl) 410 | .setName("Use new lines default value") 411 | .setDesc("Value that will be selected by default in link modal.") 412 | .addToggle((toggle) => 413 | toggle 414 | .setValue(this.plugin.settings.newLinePreset) 415 | .onChange(async (value) => { 416 | this.plugin.settings.newLinePreset = value; 417 | await this.plugin.saveSettings(); 418 | }) 419 | ); 420 | 421 | containerEl.createEl("h4", { text: "Format" }); 422 | new Setting(containerEl) 423 | .setName("Capitalize book names?") 424 | .setDesc( 425 | 'Should book names be automatically capitalized? For example "1cOr" will be turned into "1Cor".' 426 | ) 427 | .addToggle((toggle) => 428 | toggle 429 | .setValue(this.plugin.settings.shouldCapitalizeBookNames) 430 | .onChange(async (value) => { 431 | this.plugin.settings.shouldCapitalizeBookNames = value; 432 | await this.plugin.saveSettings(); 433 | }) 434 | ); 435 | 436 | containerEl.createEl("h4", { text: "Misc" }); 437 | 438 | new Setting(containerEl) 439 | .setName("Verify files?") 440 | .setDesc("Verify existence of files you are trying to link, so that you are not inserting wrong references by mistake.") 441 | .addToggle((toggle) => 442 | toggle 443 | .setValue(this.plugin.settings.verifyFilesWhenLinking) 444 | .onChange(async (value) => { 445 | this.plugin.settings.verifyFilesWhenLinking = value; 446 | await this.plugin.saveSettings(); 447 | }) 448 | ) 449 | 450 | 451 | } 452 | } 453 | --------------------------------------------------------------------------------