├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .npmrc ├── Changelog.txt ├── LICENSE ├── README.md ├── attachments ├── BracketHighlight.gif ├── Commands.png ├── Pasted_image_20230125230046.png ├── Pasted_image_20230125230351.png ├── Pasted_image_20230125230928.png ├── Pasted_image_20230125231233.png ├── Pasted_image_20230125231356.png ├── Pasted_image_20230125231644.png ├── Pasted_image_20230125231735.png ├── Pasted_image_20230125232448.png ├── Pasted_image_20230125233958.png ├── Pasted_image_20230314211417.png ├── Pasted_image_20230314211657.png ├── Pasted_image_20230314212111.png ├── Pasted_image_20230811133823.png ├── Pasted_image_20230811134737.png ├── Pasted_image_20230811134925.png ├── Pasted_image_20230811135026.png ├── Pasted_image_20230811140306.png ├── Pasted_image_20230831132418.png ├── Pasted_image_20230831134431.png ├── Pasted_image_20230831134504.png ├── Pasted_image_20230831134601.png ├── Pasted_image_20230925220351.png ├── Pasted_image_20240227234145.png ├── Pasted_image_20240228002357.png ├── Pasted_image_20240228005151.png ├── Pasted_image_20240228005240.png ├── Pasted_image_20240613130848.png ├── Pasted_image_20240613160326.png ├── Pasted_image_20240923203830.png └── SelectionMatching.gif ├── esbuild.config.mjs ├── main.js ├── manifest.json ├── package-lock.json ├── package.json ├── src ├── Const.ts ├── EditorExtensions.ts ├── GroupedCodeBlockRenderer.ts ├── ReadingView.ts ├── Settings.ts ├── SettingsTab.ts ├── Utils.ts └── main.ts ├── styles.css ├── tsconfig.json ├── version-bump.mjs └── versions.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | main.js 4 | -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | main.js linguist-generated=true 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: [ 14 | "https://www.buymeacoffee.com/ThePirateKing", 15 | ] 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /Changelog.txt: -------------------------------------------------------------------------------- 1 | - 1.2.8 (2024.09.23): 2 | New: 3 | - `lsep` (line separator), and `tsep` (text separator) parameter for text highlight 4 | - Default themes are modifiable now. You also have two options for restoring the selected or all default theme 5 | - Added two command for indenting and unindenting the code block, where the cursor is in 6 | - Added a button for selecting all the code block content in editing mode 7 | - Added a button for wrapping/unwrapping code block content in reading mode 8 | - Added option to always display the Copy code button 9 | - Added option to disable folding for code blocks, where `fold` or `unfold` was NOT defined 10 | 11 | Modified: 12 | - Line highlight and text highlight has been separated! Please read the README for more details 13 | - For text highlight and header title it is now possible to define a `"` or `'` inside the text. To do this you have to escape it with a backslash e.g.: `file:"Hello \" World!"` 14 | - Folded code blocks in editing mode now display the Copy code button in the header when hovering over the header. 15 | 16 | BugFix: 17 | - Fixed, if the first line of the code block was too long, it was not displayed correctly. The end was cut off. 18 | - Fixed an issue with Tasks plugin, where the Tasks plugin kept refreshing the tasks, when editing the document 19 | - Fixed a bug, where leading spaces (3 or less) were automatically removed in reading mode 20 | - Fixed a bug in reading mode, which wrapped lines incorrectly 21 | 22 | - 1.2.7 (2024.06.13): 23 | New: 24 | - Custom SVGs 25 | - Option to uncollapse all codeblock on printing 26 | - Bracket highlight (click next to a bracket to highlight it and its opening/closing pair) 27 | - Selection matching (select text to highlight the where the text is found in the document) 28 | - Inverse fold behavior 29 | - Option to unwrap code in reading view 30 | - Text highlight with from and to markers 31 | 32 | Modified: 33 | - Semi-fold does not count start and end lines (line with opening and closing backticks) in editing mode anymore 34 | - Hide inactive options on settings page 35 | - Performance improvements 36 | - CSS copy and delete code positioning fix 37 | - Moved border colors to language specific colors 38 | - Language specific colors can now be set for code blocks without a language as well (specify `nolang` as a language) 39 | - Fixed a few smaller bugs 40 | 41 | - 1.2.6 (2024.02.08): 42 | New: 43 | - Option to set colors language specifically. This means, you can customize now (almost) every color for Python, and totally different colors for C. Please read the README 44 | - Added option to highlight text, not just lines. Please read the README 45 | - It is possible now to automatically update links, if a file is renamed. Please read the README 46 | 47 | Modified: 48 | - Settingstab reorganized a little, because there are a lot of settings now 49 | - Every parameter works now with "=" or ":" (Example: hl:7 or hl=7) 50 | - All sorts of links work now (markdown link, wiki link, normal http or https), BUT they only work if they are marked as comments. You have to mark them as comment according to the current code block language (For example // in C/C++, # in Python etc.). This change was necessary as the processing of the HTML in Reading mode was too resource intensive 51 | - Fixed indented code blocks 52 | - Fixed minimal theme incompatibility 53 | - Moved Copy code and delete code buttons to the header (if it is present) 54 | - Improved performance in edit mode and reading mode as well 55 | - Fixed a case where in reading mode the syntax highlighting was incorrect 56 | 57 | - 1.2.5 (2023.09.26): 58 | BugFix: 59 | - Obsidian handles indentation differently when something is indented using TAB or 4 spaces, and this caused that the indented code block was not displayed correctly. Now it should work correctly. 60 | 61 | - 1.2.4 (2023.09.25): 62 | New: 63 | - You can use now links inside code blocks, and the header 64 | - Code blocks in a list are now indented properly in editing mode as well. Read more here 65 | 66 | BugFix: 67 | - Fixed a bug, which caused that the copy code button did not copy the whole text from a code block 68 | 69 | - 1.2.3 (2023.09.11): 70 | BugFix: 71 | - content of code blocks without a language specified are now copied correctly 72 | 73 | - 1.2.2 (2023.09.10): 74 | New options: 75 | - You can now exclude specific code blocks by defining the exclude parameter 76 | - You can now enable/disable the plugin in source mode 77 | - Added option to display indentation lines in reading view 78 | - Lines in reading view can now be collapsed 79 | - Added option to display a copy code button. This is very similar to the default Obsidian copy code button, with the difference that it will be always shown, even if you click inside a code block 80 | 81 | - 1.2.1 (2023.08.31): 82 | BugFix: 83 | - Nested code blocks are handled correctly now 84 | - Fixed some CSS problems with themes 85 | - Fixed display problem in embedded code blocks 86 | - Fixed not displaying code blocks in call outs in editing mode 87 | - Fixed that annoying bug, where the cursor jumps before the header when the header is clicked and a few other smaller problems 88 | 89 | New featrue: 90 | - Semi-folding! 91 | 92 | Note: 93 | - Commands need a little tweaking. Next release will fix them. 94 | 95 | - 1.2.0 (2023.08.11): 96 | Unfortunately, you'll have to remove the data.json file or reinstall the plugin, but this was necessary. Thanks for your understanding. 97 | New features: 98 | - option to show a delete code button (this actually deletes the code!) 99 | - collapse icon position 100 | - collapsed code text 101 | - active line number color 102 | - PDF print settings 103 | - inline code styling 104 | - border colors 105 | - Bug fixing: a lot... 106 | 107 | - 1.1.9 (2023.05.20): 108 | - Mostly styling 109 | 110 | - 1.1.8 (2023.05.13): 111 | - BugFix: Fixed a bug, where under Linux the color picker was not displayed. 112 | 113 | - 1.1.7 (2023.05.04): 114 | - BugFix: Incorrect display of the header when using minimal theme and "Readable line length" was enabled 115 | - BugFix: Printing to a PDF did not work until now. Now it works. 116 | - New: It is possible to use wildcard ( * ) for excluding languages. e.g.: ad-* will exclude every codeblock where the language starts with ad- (ad-example, ad-bug, ad-summary etc.). The wildcard can be either at the beginning or at the end. 117 | 118 | - 1.1.6 (2023.04.23): 119 | - BugFix: Incorrectly handled inline code in ReadingView 120 | 121 | - 1.1.5 (2023.03.21): 122 | - BugFix: Fixed the bug I mentioned last time, and a few other things, rewrote and removed unnecessary code. Everything should work as it should 123 | 124 | - 1.1.4 (2023.03.19): 125 | - Found a very strange bug, but most people won't even notice it. I added a workaround which unfortunately is not a 100% percent solution (maybe around 90%). This bug however originates either from Obsidian or CodeMirror itself. I am still investigating. 126 | - The bug: if a document is opened (only in editing mode), then in very rare cases the viewport is not processed to the end. This results, that the linenumbers, background colors, and other styles are not set for those code block lines. As I said, it occurs in very rare cases, and the workaround helps, but it is not a permanent solution. 127 | 128 | - 1.1.3 (2023.03.18): 129 | - BugFix: Fixed a minor bug in ReadingView 130 | 131 | - 1.1.2 (2023.03.17): 132 | - BugFix: corrected minor bug in ReadingView. 133 | 134 | - 1.1.1 (2023.03.16): 135 | BugFix: Corrected two small bugs in reading mode: 136 | - line number was displayed incorrectly when a very long line was displayed which overflowed to multiple lines. 137 | - When the header was collapsed and below another codeblock was displayed without header, it appeared as it belonged to the header. 138 | 139 | - 1.1.0 (2023.03.14): 140 | - New feature: Display codeblock language icon in header 141 | - New feature: Add alternative highlight colors 142 | - Change: Line numbers in editing mode are displayed just as line numbers in reading mode. This change was necessary. 143 | - BugFix: Fixed a bug, which caused the text in the header to be displayed always in lower case. 144 | - BugFix: Fixed a bug, which caused unnecessary execution. 145 | 146 | - 1.0.2 (2023.02.07): 147 | - Implemented changes recommended by the Obsidian team. 148 | 149 | - 1.0.1 (2023.01.29): 150 | - Corrected that empty lines were not shown in reading mode, and the theme is set now automatically according to the Obsidian theme. 151 | 152 | - 1.0.0 (2023.01.26): 153 | - Initial release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 mugiwara85 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Codeblock Customizer Plugin

2 | 3 |

4 | release 5 | 6 |

7 | 8 | > [!important] 9 | > Version `1.2.8` changes 10 | > 11 | > New: 12 | > - `lsep` (line separator), and `tsep` (text separator) parameter for text highlight 13 | > - Default themes are modifiable now. You also have two options for restoring the selected or all default theme 14 | > - Added two command for indenting and unindenting the code block, where the cursor is in 15 | > - Added a button for selecting all the code block content in editing mode 16 | > - Added a button for wrapping/unwrapping code block content in reading mode 17 | > - Added option to always display the Copy code button 18 | > Added option to disable folding for code blocks, where `fold` or `unfold` was NOT defined 19 | > 20 | > Modified: 21 | > - Line highlight and text highlight has been separated! Please read the README for more details 22 | > - For text highlight and header title it is now possible to define a `"` or `'` inside the text. To do this you have to escape it with a backslash e.g.: `file:"Hello \" World!"` 23 | > - Folded code blocks in editing mode now display the Copy code button in the header when hovering over the header. 24 | > 25 | > BugFix: 26 | > - Fixed, if the first line of the code block was too long, it was not displayed correctly. The end was cut off. 27 | > - Fixed an issue with Tasks plugin, where the Tasks plugin kept refreshing the tasks, when editing the document 28 | > - Fixed a bug, where leading spaces (3 or less) were automatically removed in reading mode 29 | > - Fixed a bug in reading mode, which wrapped lines incorrectly 30 | 31 | 32 | This is a plugin for Obsidian (https://obsidian.md). 33 | 34 | The plugin lets you customize the code blocks in the following way: 35 | - Default Obsidian and Solarized theme. You can create your own themes as well. 36 | - Enable editor active line highlight. The active line in Obsidian (including code blocks) will be highlighted (you can customize the color). 37 | - Enable code block active line highlight. The active line inside a code block will be highlighted (you can customize the color). 38 | - Exclude languages. You can define languages separated by a comma, to which the plugin will not apply. You can also define `exclude` for specific code blocks to exclude them. 39 | - Set background color for code blocks. 40 | - Lets you highlight specific lines. 41 | - Customize highlight color 42 | - Lets you define multiple highlight colors to highlight lines. 43 | - Display filename 44 | - If a filename is defined a header will be inserted, where it is possible to customize the text (color, bold, italic), and the header (color, header line) itself as well 45 | - Fold code 46 | - If the header is displayed (either by defining filename or other way explained below), you can click on the header to fold the code block below it 47 | - Display code block language. This displays the language (if specified) of the code block in the header. 48 | - Customize text color, background color, bold text, italic text for the language tag inside the header. 49 | - By default the language tag is only displayed, if the header is displayed, and a if a language is defined for a code block. You can however force, to always display the code block language, even if the header would not be displayed. 50 | - Display code block language icon (if available for the specified language) in the header. 51 | - Add line numbers to code blocks 52 | - Customize if the line number should also be highlighted, if a line is highlighted 53 | - Customize background color, and color of the line numbers 54 | - and much more... 55 | 56 | ## Parameters 57 | 58 | Parameters can be defined in the opening line of a code block (after the three opening backticks). 59 | All parameters can be defined using `:` or `=`. 60 | Available parameters: 61 | 62 | | Name | Value | Description | 63 | | ------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 64 | | fold | | Defines that the code block is in folded state when the document is opened. | 65 | | unfold | | Defined that the code block is in unfolded state when the document is opened and the `Inverse fold behavior` option is enabled, otherwise ignored. | 66 | | exclude | | Exclude the code block from the plugin. | 67 | | hl | Multiple | **Everything that applies to the `hl` parameter also applies to alternative highlight colors!**
Multiple values can be combined with a `,` (e.g: `hl:test,3,5-6,9\|abc,test1,test2,10-15\|test3`)
Highlights specified **lines** on different formats: | 68 | | | hl:{number} | `hl:5` - Highlights line 5. | 69 | | | hl:{range} | `hl:5-7` - Highlights lines from 5 to 7. | 70 | | | hl:{string} | `hl:test` - Highlights all lines containing the word `test`. | 71 | | | hl:{number}\|{string} | `hl:5\|test` - Highlights line 5 if it contains the word `test`. | 72 | | | hl:{range}\|{string} | `hl:5-7\|test` - Highlights lines 5 to 7 if they contain the word `test`. | 73 | | hlt | Multiple | **Everything that applies to the `hlt` parameter also applies to alternative highlight colors!**
Multiple values can be combined with a `,` just like for the `hl` parameter.
Highlights specified **text** on different formats: | 74 | | | hlt:{string} | `hlt:test` - Highlights the word `test` in every line it is found. | 75 | | | hlt:{number} | `hlt:5` - Highlights all the text in line 5. | 76 | | | hlt::{string} | `hlt::xyz` - If the start position is not defined but the end position is, the text will be highlighted from the beginning of the line up to the end position. | 77 | | | hlt:{string}: | `hlt:abc:` - If the start position is defined, but the end position is not, the text will be highlighted from the start position, until the end of the line. | 78 | | | hlt:{string}:{string} | `hlt:abc:xyz` - Highlights text in lines starting with `abc` and ending with `xyz`. | 79 | | | hlt:{number}\|... | All the above options can be prepended with an optional number and a `\|` to specify in which line to highlight the text. | 80 | | | hlt:{range}\|... | All the above options can be prepended with an optional range and a `\|` to specify in which range to highlight the text. | 81 | | lsep | char | Line separator. Optionally you can define a line separator (a single character) for text highlight instead of the default `\|`. Useful, if you want to highlight text starting and/or ending with `\|`. This can be set globally as well. | 82 | | tsep | char | Text separator. Optionally you can define a text separator (a single character) for text highlight instead of the default `:`. Useful, if you want to highlight text starting and/or ending with `:`. This can be set globally as well. | 83 | | file | {string} | Sets the display text for the header. (e.g: `file:hello` or `file:"Hello World!"`) | 84 | | title | {string} | Alias for `file` | 85 | | ln | Multiple | | 86 | | | true | Displays line numbers for that specific code block, even if `Enable line numbers` is disabled | 87 | | | false | Does not display line numbers for that specific code block, even if `Enable line numbers` is enabled | 88 | | | {number} | Sets the offset for line number to start (e.g: `ln:5` -> line numbering starts from 5) | 89 | 90 | ## Themes 91 | 92 | The plugin comes with a default Obsidian and a Solarized theme. The default theme is Obsidian. 93 | 94 | Default Solarized theme (dark mode): 95 | 96 | ![Pasted_image_20230125231644.png](attachments/Pasted_image_20230125231644.png) 97 | 98 | Default Solarized theme (light mode): 99 | 100 | ![Pasted_image_20230125231735.png](attachments/Pasted_image_20230125231735.png) 101 | 102 | How the themes work: 103 | - Every setting and color is saved in the theme, except the excluded languages. 104 | - You can modify the default themes (there is an option to restore them to default), but you can't delete them. 105 | - Save your changes! 106 | - Each theme has its own light and dark colors. To customize the light/dark colors, just switch Obsidian to light/dark mode, and you can change the colors for that mode. 107 | - When creating a new theme the currently selected theme will be taken as a template for the new theme. 108 | - After saving changes in a theme, these become the new default values. Example: You select a color (red) and save the theme. Now, this color is the default value. This means, that if you click the "restore default color" icon next to the color picker the color red will be restored. 109 | 110 | ## Highlighting 111 | 112 | ### Main highlight 113 | 114 | To highlight lines specify `hl:` followed by line numbers in the first line of the code block. 115 | - You can specify a single line numbers separated with a comma e.g.: `hl:1,3,5,7`. This would highlight the specified lines 116 | - You can specify ranges e.g.: `hl:2-5` This would highlight lines from 2 to 5. 117 | - You can specify a string e.g.: `hl:test`. This would highlight all lines containing the word test. You can also prepend this value with a line number, or range and a `|` like this: `hl:5|test,5-7|test2` 118 | - You can also combine the methods e.g.: `hl:1,3,4-6,test,5|test,7-9|test3` This would highlight lines 1, 3 and lines from 4 to 6. 119 | 120 | Example: 121 | ` ```cpp hl:1,3,4-6` 122 | 123 | ![Pasted_image_20230125230046.png](attachments/Pasted_image_20230125230046.png) 124 | 125 | ### Alternative highlight 126 | 127 | You can define multiple highlight colors. This means, that you have to define a name for the highlight color. This name will be used as a parameter, and you can use it just like with the `hl` parameter. 128 | 129 | Example: 130 | You define three types of highlight colors (`info`, `warn`, `error`). After that set the colors. After that you can use it in the first line of code blocks: 131 | ` ```cpp info:2 warn:4-6 error:8` 132 | 133 | ![Pasted_image_20230811133823.png](attachments/Pasted_image_20230811133823.png) 134 | 135 | Example code block with multiple highlight colors: 136 | 137 | ![[Pasted_image_20230314211417.png]](attachments/Pasted_image_20230314211417.png) 138 | 139 | ### Text highlight 140 | 141 | It is possible now to highlight text instead of lines. To use this feature use the `hlt` parameter. 142 | * If after the `hlt:` parameter a string is defined, then the string is highlighted in every line it is present in the code block. Example: `hl:extension` 143 | * If after the `hlt:` parameter a number is defined, then all words in the specified lines are highlighted . Example: `hl:5` 144 | * If after the `hlt:` parameter a number is defined, followed by a pipe `|`, followed by a string, then the word is highlighted only in this line if it is present. Example: `hl:9|print` 145 | * If after the `hlt:` parameter a range is defined, followed by a pipe `|` character, followed by a string, then the word is highlighted only in these line ranges, if it present. Example: `hl:6-7|print` 146 | * If after the `hlt:` parameter a string, followed by a `:`, followed by a string is defined, then the string will be highlighted which starts with the string before the `:`, and ends with the string after `:` e.g.: `hlt::`. Example: 147 | * `hlt:abc:` -> startString is defined, but endString is not defined. This will highlight the text starting with `startString` until the end of the line 148 | * `hlt::xyz` -> startString is not defined, but endString is defined. This will highlight the text starting from the beginning of the line until `endString` 149 | * `hlt:abc:xyz` -> highlights text starting with `abc` and ending with `xyz` in all lines it is present 150 | 151 | All the above options can be prepended with an optional number or range and a `|` to specify in which line to highlight the text. 152 | 153 | > [!note] 154 | > - You can use the text highlighting with alternative highlight colors as well! 155 | > - For every alternative highlight color a new text highlight parameter can be used. For example, if you created an alternative highlight color called `error`, then you can use the `error` parameter for line highlighting. To highlight text simply append a `t` after the alternative color name. This means that if you want to highlight text using the `error` color, you'll have to use the `errort` (note the `t` at the end) of the parameter. 156 | > - If you want to highlight text which contains a `"` or `'` you'll have to escape it with a basckslash. For example: `hlt:start\"text:end\"text` or `hlt:"start \" text:end \" text"` 157 | > - If you want to highlight text which contains default line separator `|` or the default text separator `:`, you can redefine them, with the `lsep` and `tsep` parameters. For example: `hlt:5^|ˇ: lsep:^ tsep:ˇ` would highlight text from `|` to `:`. You can also globally define them 158 | 159 | 160 | An example code block with text highlight, using three different colors is shown below: 161 | 162 | ![[Pasted_image_20240227234145.png]](attachments/Pasted_image_20240227234145.png) 163 | 164 | An example code block with text highlight, using from and to markers: 165 | 166 | ![[Pasted_image_20240923203830.png]](attachments/Pasted_image_20240923203830.png) 167 | 168 | ## Language specific coloring 169 | 170 | In the settings, on the `Language specific colors` settings page it is now possible to define colors for languages. These colors will only apply to code blocks with the defined language. If you defined colors for "Python", then those colors will only apply to every Python code block. **If you want to specify colors for code blocks which do not have a language defined, specify `nolang` as a language.** 171 | First, you have to add a language. Then you can select which color you want to set. Available options are: 172 | - Code block active line color 173 | - Code block background color 174 | - Code block border color 175 | - Code block text color 176 | - Matching bracket color 177 | - Non-matching bracket color 178 | - Matching bracket background color 179 | - Non-matching bracket background color 180 | - Selection match highlight color 181 | - Header background color 182 | - Header text color 183 | - Header line color 184 | - Header language text color 185 | - Header language background color 186 | - Gutter text color 187 | - Gutter background color 188 | - Gutter active line number color 189 | 190 | An example is shown below, where the background color has been defined for "Python", "JavaScript" and "C++" languages. 191 | 192 | ![[Pasted_image_20240228002357.png]](attachments/Pasted_image_20240228002357.png) 193 | 194 | Example code blocks with border colors set: 195 | 196 | ![Pasted_image_20230811134737.png](attachments/Pasted_image_20230811134737.png) 197 | 198 | **Don't forget to set `Codeblock border styling position`, otherwise border colors will not be displayed!** 199 | 200 | ## Display filename/title 201 | 202 | To display a filename specify `file:` or `title:` followed by a filename in the first line of the code block. If the filename contains space, specify it between `""` e.g.: `file:"long filename.cpp"`. `title` is basically an alias for file. If both are defined, then `file` will be used 203 | 204 | Example: 205 | ` ```cpp file:test.cpp` 206 | ` ```cpp title:test.py` 207 | ` ```cpp file:"long filename.cpp"` 208 | 209 | ![Pasted_image_20230125230351.png](attachments/Pasted_image_20230125230351.png) 210 | 211 | If you want to display text which contains a `"` or `'` you'll have to escape it with a backslash. For example: `file:"Hello \" World!"` 212 | 213 | ## Folding 214 | 215 | If the header is displayed, simply clicking on it, will fold the code block below it. 216 | ### Default fold 217 | 218 | To specify an initial fold state when the document is opened, specify `fold` in the first line of the code block. If `fold` is defined in a code block, then when you open the document, the code block will be automatically collapsed, and only the header will be displayed. You can unfold the code block by clicking on the header. 219 | 220 | Example: 221 | ` ```cpp fold` 222 | 223 | ![Pasted_image_20230125230928.png](attachments/Pasted_image_20230125230928.png) 224 | 225 | ### Semi-fold 226 | 227 | You can enable semi-folding in settings tab: 228 | ![Pasted_image_20230831132418.png](attachments/Pasted_image_20230831132418.png) 229 | 230 | After enabling it, you have to select the count of the visible lines (default is 5). Optionally you can also enable an additional uncollapse button, which will be displayed in the last line. 231 | Semi-fold works just like the normal fold with the following differences: 232 | - If your code block doesn't have minimum required lines, then it will fold as until now. 233 | - If your code block does have the minimum required line (count of visible lines + 4 for fading effect), then it will semi-fold. 234 | 235 | The number of the "fade" lines is constant, and cannot be changed. 236 | Example: You set the count of visible lines to 5, and you have a code block with 10 lines. In this case semi-fold will be used. The first 5 lines remain visible, and the next 4 lines will "fade away". 237 | 238 | >[!note] 239 | >In editing mode the opening and closing lines (with the three backticks) do not count! 240 | 241 | Example semi-folded code block (light theme): 242 | ![Pasted_image_20230831134504.png](attachments/Pasted_image_20230831134504.png) 243 | 244 | Example semi-folded code block (dark theme): 245 | ![Pasted_image_20230831134431.png](attachments/Pasted_image_20230831134431.png) 246 | 247 | Example semi-folded code block with additional uncollapse button: 248 | ![Pasted_image_20230831134601.png](attachments/Pasted_image_20230831134601.png) 249 | 250 | 251 | ### Inverse fold behavior 252 | 253 | When this options is enabled in the settings page, code blocks are collapsed by default when a document is opened, even if `fold` was **NOT** defined. If you enabled this option, and want some code blocks unfolded by default, you can use the `unfold` parameter. 254 | 255 | ## Icon 256 | 257 | There are currently around 170 icons available for different languages. You can enable the option in the settings page to display icons in the header. If you enable this option, and if the language specified in the code block has an icon, and the header is displayed, then the icon will be displayed. You can also force to always display the icon (which also means that the header will be also displayed) even if the header is not displayed, because the `file` parameter is not defined. 258 | 259 | ## Header 260 | 261 | The header is displayed in the following cases: 262 | - You specified a `file:` or `title:` 263 | - You specified `fold`. If you specified `fold` but did not specify `file:` or `title:` a default text `Collapsed code` will be displayed on the header 264 | - You enabled the `Always display codeblock language` or the `Always display codeblock language icon` option in settings, but did not specify `file:` or `title:` or `fold` 265 | 266 | If the header is displayed, folding works as well. If `Always display codeblock language` is enabled then the header will display the code block language as well. 267 | 268 | Example: 269 | - Header with fold only 270 | 271 | ![Pasted_image_20230125233958.png](attachments/Pasted_image_20230125233958.png) 272 | - Header with code block language only 273 | 274 | ![Pasted_image_20230125231233.png](attachments/Pasted_image_20230125231233.png) 275 | - Header with code block language and filename/title as well 276 | 277 | ![Pasted_image_20230125231356.png](attachments/Pasted_image_20230125231356.png) 278 | - Header with code block language, filename/title and icon as well 279 | 280 | ![[Pasted_image_20230314212111.png]](attachments/Pasted_image_20230314212111.png) 281 | 282 | ## Line numbers 283 | 284 | To enable line numbers go to the plugin settings and enable the `Enable line numbers` option. After that the line numbers will be displayed before code blocks. 285 | 286 | Example: 287 | 288 | ![[Pasted_image_20230314211657.png]](attachments/Pasted_image_20230314211657.png) 289 | 290 | Example for reading mode: 291 | 292 | ![Pasted_image_20230125232448.png](attachments/Pasted_image_20230125232448.png) 293 | 294 | ### ln parameter 295 | 296 | The `ln:` parameter can have 3 values: `true`, `false`, `number` 297 | - If `ln` is set to `ln:true`, then for that specific code block only, the line numbers will be displayed, even if line numbers are not enabled in the settings. 298 | - If `ln` is set to `ln:false`, then for that specific code block only, the line numbers will NOT be displayed, even if line number are enabled in the settings. 299 | - If `ln` is set to a number, e.g. `ln:5`, then it sets the offset for the line numbers. 300 | 301 | ![Pasted_image_20230811140306.png](attachments/Pasted_image_20230811140306.png) 302 | 303 | ## Commands 304 | 305 | There are three commands available in the command palette. You can: 306 | - fold all code blocks in the current document, 307 | - unfold all code blocks in the current document, 308 | - restore original state of code blocks 309 | 310 | If you collapsed/uncollapsed all code blocks there is no need to restore them to their original state. When you switch documents they are automatically restored to their original state. 311 | 312 | ![Commands.png](attachments/Commands.png) 313 | 314 | ## Inline code 315 | 316 | If you want to style inline code, you have to enable it first. After that you can set the background color and the text color for inline code. 317 | 318 | ![Pasted_image_20230811134925.png](attachments/Pasted_image_20230811134925.png) 319 | 320 | ## Print to PDF 321 | 322 | By default, if you print a document the styling is not applied to it. You can enable it in the settings. By default, the light colors are used for printing, but if you want to force the dark colors, you can enable the second toggle. 323 | 324 | ![Pasted_image_20230811135026.png](attachments/Pasted_image_20230811135026.png) 325 | 326 | ## Indented code blocks 327 | 328 | Code blocks in **lists**, are now indented properly as shown below. Simply, mark the text in the code block, and press TAB. This will shift the code block right, by adding margin to the left side. Pressing TAB multiple times, indents the code block more. If you want to undo it, just select the text again, and press SHIFT+TAB. 329 | 330 | ![Pasted_image_20230925220351.png](attachments/Pasted_image_20230925220351.png) 331 | 332 | ## Links 333 | 334 | If you want to convert markdown, wiki and normal http/https link syntax to actual links inside code blocks, then you have to mark them as comment according to the current code block language, and enable the setting `Enable links usage` in the settings on the "Codeblock" settings page. **Links can also be used in the header.** 335 | For example if you are in a python code block, then you have write a "#" before the link (comment it out), and it will be automatically converted to a link. 336 | **From version `1.2.6` only commented out links will be converted!** 337 | By default the links, which point to another document in your vault, are not updated, if you rename the file. This is because Obsidian does not provide (yet) a way to add these links to the metadata cache. A temporary solution for that is to enable the option `Enable automatically updating links on file rename` option in settings. 338 | 339 | >[!important] 340 | >Please note, that this method iterates over all of your documents in you vault! If you have a huge vault, it could take some time. 341 | >During my testing, it was however, very efficient, but please test it yourself! 342 | 343 | Sample code block with links, but with the option `Enable links usage` disabled: 344 | 345 | ![[Pasted_image_20240228005151.png]](attachments/Pasted_image_20240228005151.png) 346 | 347 | Same code block with the `Enable links usage` option enabled: 348 | 349 | ![[Pasted_image_20240228005240.png]](attachments/Pasted_image_20240228005240.png) 350 | 351 | ## Custom SVGs 352 | 353 | It is possible to use custom SVGs, and apply custom syntax highlighting for code blocks. To use this feature create the following folder `\.obsidian\plugins\codeblock-customizer\customSVG`. In this folder create a file called `svg.json` with similar content: 354 | 355 | ```json 356 | { 357 | "languages": [ 358 | { 359 | "codeblockLanguages": ["language1", "language2"], 360 | "displayName": "iRule", 361 | "svgFile": "f5.svg", 362 | "format": "tcl" 363 | } 364 | ] 365 | } 366 | ``` 367 | 368 | Explanation: 369 | - codeblockLanguages (**required**): one or more languages, where you want to apply the displayName, SVG, and format. 370 | - displayName (**required**): the display Language, which is displayed in the header. 371 | - svgFile (**optional**): name of an SVGfile in the same folder. **The file must be a plain text SVG without the SVG tag itself**. Look at [Const.ts](src/Const.ts) for examples. 372 | - format (**optional**): the syntax highlighting to apply for this code block. 373 | 374 | >[!important] 375 | >Obsidian uses two different methods for syntax highlighting. For editing mode it uses CodeMirror 6, and for reading mode it uses Prism.js. Because of this there is a slight difference between how this works. 376 | >- If you want to apply syntax highlighting in editing mode, then the codeblockLanguage **must NOT** have syntax highlighting, because it is not possible (or I didn't found a way) to overwrite any existing syntax highlighting. For example `language1` does not have syntax highlighting, therefore the `tcl` syntax highlighting will be applied successfully. The languages specified in `codeblockLanguages` are **case sensitive** in editing mode 377 | >- In reading mode however it is possible to overwrite existing syntax highlighting. So you can apply C++ syntax highlighting for a python code block. The languages specified in `codeblockLanguages` are **NOT case sensitive** in reading mode. 378 | 379 | An example using the above shown JSON file, where `tcl` syntax highlighting is applied to `language1` code blocks, using the custom SVG file, and the custom display name: 380 | 381 | ![[Pasted_image_20240613160326.png]](attachments/Pasted_image_20240613160326.png) 382 | 383 | ## Bracket highlight 384 | 385 | You can enable bracket highlighting for matching and also for non-matching brackets. If you click next to a bracket (`(,),{,},[,]`), then the bracket itself, and the corresponding opening/closing bracket will be highlighted. You can set individual background, and highlight colors for matching and non-matching brackets: 386 | 387 | ![[Pasted_image_20240613130848.png]](attachments/Pasted_image_20240613130848.png) 388 | 389 | Below is a simple example. Notice that that the matching and non-matching bracket are highlighted with different colors: 390 | 391 | ![[BracketHighlight.gif]](attachments/BracketHighlight.gif) 392 | 393 | ## Selection matching 394 | 395 | If you enable selection matching, you can set the background color for the matches to be highlighted with. Simply select a string, or double click on a word, and the same text will be highlighted if found: 396 | 397 | ![[SelectionMatching.gif]](attachments/SelectionMatching.gif) 398 | 399 | >[!note] 400 | >Selection matching (currently) has a limit of 750 matches. If there are more matches than this, then selection matching will not highlight anything. Should you encounter a case where this number is not enough, contact me, and I'll increase it. 401 | 402 | ## How to install the plugin 403 | 404 | - Simply install directly from Obsidian 405 | - or you can just copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/codeblock-customizer/`. 406 | 407 | ## Support 408 | 409 | If you like this plugin, and would like to help support continued development, use the button below! 410 | 411 | 412 | -------------------------------------------------------------------------------- /attachments/BracketHighlight.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/BracketHighlight.gif -------------------------------------------------------------------------------- /attachments/Commands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Commands.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20230125230046.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20230125230046.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20230125230351.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20230125230351.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20230125230928.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20230125230928.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20230125231233.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20230125231233.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20230125231356.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20230125231356.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20230125231644.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20230125231644.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20230125231735.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20230125231735.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20230125232448.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20230125232448.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20230125233958.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20230125233958.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20230314211417.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20230314211417.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20230314211657.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20230314211657.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20230314212111.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20230314212111.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20230811133823.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20230811133823.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20230811134737.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20230811134737.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20230811134925.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20230811134925.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20230811135026.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20230811135026.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20230811140306.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20230811140306.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20230831132418.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20230831132418.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20230831134431.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20230831134431.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20230831134504.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20230831134504.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20230831134601.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20230831134601.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20230925220351.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20230925220351.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20240227234145.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20240227234145.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20240228002357.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20240228002357.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20240228005151.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20240228005151.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20240228005240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20240228005240.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20240613130848.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20240613130848.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20240613160326.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20240613160326.png -------------------------------------------------------------------------------- /attachments/Pasted_image_20240923203830.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/Pasted_image_20240923203830.png -------------------------------------------------------------------------------- /attachments/SelectionMatching.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugiwara85/CodeblockCustomizer/cde7230697f09eaa5ee2c7e9b41d824c23b2ec8a/attachments/SelectionMatching.gif -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | 5 | const banner = 6 | `/* 7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 8 | if you want to view the source, please visit the github repository of this plugin 9 | */ 10 | `; 11 | 12 | const prod = (process.argv[2] === "production"); 13 | 14 | const context = await esbuild.context({ 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: ["src/main.ts"], 19 | bundle: true, 20 | external: [ 21 | "obsidian", 22 | "electron", 23 | "@codemirror/autocomplete", 24 | "@codemirror/collab", 25 | "@codemirror/commands", 26 | "@codemirror/language", 27 | "@codemirror/lint", 28 | "@codemirror/search", 29 | "@codemirror/state", 30 | "@codemirror/view", 31 | "@lezer/common", 32 | "@lezer/highlight", 33 | "@lezer/lr", 34 | ...builtins], 35 | format: "cjs", 36 | target: "es2018", 37 | logLevel: "info", 38 | sourcemap: prod ? false : "inline", 39 | treeShaking: true, 40 | outfile: "main.js", 41 | }); 42 | 43 | if (prod) { 44 | await context.rebuild(); 45 | process.exit(0); 46 | } else { 47 | await context.watch(); 48 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "codeblock-customizer", 3 | "name": "Codeblock Customizer", 4 | "version": "1.2.8", 5 | "minAppVersion": "0.15.0", 6 | "description": "This Obsidian plugin lets you customize your codeblocks in editing, and reading mode as well.", 7 | "author": "mugiwara", 8 | "authorUrl": "https://github.com/mugiwara85", 9 | "fundingUrl": "https://www.buymeacoffee.com/ThePirateKing", 10 | "isDesktopOnly": false 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-sample-plugin", 3 | "version": "1.0.0", 4 | "description": "This is a sample plugin for Obsidian (https://obsidian.md)", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/dompurify": "^3.0.3", 16 | "@types/lodash": "^4.14.197", 17 | "@types/node": "^16.11.6", 18 | "@types/validator": "^13.12.0", 19 | "@typescript-eslint/eslint-plugin": "5.29.0", 20 | "@typescript-eslint/parser": "5.29.0", 21 | "builtin-modules": "3.3.0", 22 | "esbuild": "0.17.3", 23 | "eslint": "^8.39.0", 24 | "obsidian": "latest", 25 | "tslib": "2.4.0", 26 | "typescript": "4.7.4" 27 | }, 28 | "dependencies": { 29 | "@codemirror/language": "^6.8.0", 30 | "@codemirror/search": "^6.5.6", 31 | "@lezer/common": "^1.1.0", 32 | "@simonwep/pickr": "^1.8.2", 33 | "hast-util-from-html": "^2.0.1", 34 | "hast-util-to-html": "^9.0.0", 35 | "lodash": "^4.17.21", 36 | "unist-util-visit-parents": "^6.0.1", 37 | "validator": "^13.12.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/GroupedCodeBlockRenderer.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownRenderChild, MarkdownView } from "obsidian"; 2 | import { getLanguageIcon, createCodeblockIcon, createCodeblockLang, getDisplayLanguageName, Parameters, getLanguageSpecificColorClass, getDefaultParameters, getCurrentMode, createContainer, createFileName, createCodeblockCollapse, getBorderColorByLanguage, getPropertyFromLanguageSpecificColors } from "./Utils"; 3 | import { createButtons, toggleFold } from "./ReadingView"; 4 | import { fadeOutLineCount } from "./Const"; 5 | import CodeBlockCustomizerPlugin from "./main"; 6 | 7 | export class GroupedCodeBlockRenderChild extends MarkdownRenderChild { 8 | private view: MarkdownView; 9 | private clickListeners: Array<() => void> = []; 10 | private childMap: Map; 11 | private observer: MutationObserver | null = null; 12 | private debouncedProcess: () => void; 13 | private plugin: CodeBlockCustomizerPlugin; 14 | private hoverListeners: Array<() => void> = []; 15 | private activeReadingViewTabs: Map> = new Map(); 16 | 17 | constructor(containerEl: HTMLElement, view: MarkdownView, childMap: Map, plugin: CodeBlockCustomizerPlugin) { 18 | super(containerEl); 19 | this.view = view; 20 | this.childMap = childMap; 21 | this.plugin = plugin; 22 | this.debouncedProcess = debounce(() => this.processGroupedCodeBlocks(), 50, false); 23 | } 24 | 25 | async onload() { 26 | this.processGroupedCodeBlocks(); 27 | this.setupMutationObserver(['class', 'groupname']); 28 | }// onload 29 | 30 | onunload() { 31 | this.childMap.delete(this.view); 32 | this.cleanupListeners(); 33 | this.disconnectObserver(); 34 | }// onunload 35 | 36 | public processGroupedCodeBlocks() { 37 | this.cleanup(); 38 | 39 | const allCodeBlockContainers: NodeListOf = this.containerEl.querySelectorAll('.el-pre.codeblock-customizer-pre-parent'); 40 | if (allCodeBlockContainers.length === 0) { 41 | this.reconnectObserver(['class']); 42 | return; 43 | } 44 | 45 | const consecutiveGroups = this.getConsecutiveGroups(allCodeBlockContainers); 46 | const processedGroupNames = new Set(); 47 | 48 | consecutiveGroups.forEach((group) => { 49 | const isGroupedBlock = group[0].classList.contains('codeblock-customizer-grouped'); 50 | const groupName = group[0].getAttribute('groupname'); 51 | 52 | if (isGroupedBlock && group.length > 1 && groupName && !processedGroupNames.has(groupName)) { 53 | processedGroupNames.add(groupName); 54 | this.processGroup(group, groupName); 55 | } else { 56 | group.forEach(blockElement => { 57 | blockElement.style.display = ''; // is this needed? 58 | }); 59 | } 60 | }); 61 | 62 | this.reconnectObserver(['class']); 63 | }// processGroupedCodeBlocks 64 | 65 | private cleanup() { 66 | this.disconnectObserver(); 67 | 68 | this.containerEl.querySelectorAll('.codeblock-customizer-header-group-container').forEach(header => header.remove()); 69 | this.containerEl.querySelectorAll('.markdown-rendered .codeblock-customizer-header-group-tabs').forEach(tabs => tabs.remove()); 70 | 71 | this.cleanupListeners(); 72 | }// cleanup 73 | 74 | private processGroup(group: HTMLPreElement[], groupName: string) { 75 | const firstBlock = group[0]; 76 | 77 | this.hideGroupedCodeBlocks(group); 78 | 79 | const parameters = this.getParametersFromElement(firstBlock); 80 | const sourcePath = firstBlock.getAttribute('sourcepath') || ''; 81 | const header = this.createHeader(parameters, groupName) 82 | const frag = document.createDocumentFragment(); 83 | 84 | let languageIconElement: HTMLElement; 85 | if (parameters.displayLanguage) { 86 | const Icon = getLanguageIcon(parameters.displayLanguage); 87 | languageIconElement = Icon ? createCodeblockIcon(parameters.displayLanguage) : createCodeblockIcon("NoIcon"); 88 | } else { 89 | languageIconElement = createCodeblockIcon("NoIcon"); 90 | } 91 | header.appendChild(languageIconElement); 92 | 93 | let collapseIconElement: HTMLElement | null = null; 94 | const fileNameContainer = createFileName(parameters.headerDisplayText, this.plugin.settings.SelectedTheme.settings.codeblock.enableLinks, sourcePath, this.plugin); 95 | const groupButtonsContainer = createDiv({ cls: `codeblock-customizer-button-container` }); 96 | 97 | const updateGroupHeader = (currentBlock: HTMLPreElement) => { 98 | const currentParameters = this.getParametersFromElement(currentBlock); 99 | this.updateHeaderLanguageClasses(header, currentParameters.language); 100 | this.updateHeaderLanguageSpecificClasses(header, currentParameters.language); 101 | languageIconElement = this.updateHeaderLanguageIcon(header, languageIconElement, currentParameters.displayLanguage); 102 | this.updateHeaderButtons(groupButtonsContainer, currentParameters, currentBlock); 103 | collapseIconElement = this.updateHeaderCollapseIcon(collapseIconElement, header, currentBlock, currentParameters); 104 | this.updateHeaderFileName(fileNameContainer, currentParameters.headerDisplayText); 105 | }; 106 | 107 | const documentPath = this.view.file?.path || 'unknown_document'; 108 | const tabsContainer = this.addTabs(frag, group, updateGroupHeader, groupName, documentPath ); 109 | 110 | frag.appendChild(fileNameContainer); 111 | frag.appendChild(groupButtonsContainer); 112 | header.appendChild(frag); 113 | 114 | const currentlyActiveBlock = group[this.getStoredTabIndex(groupName, documentPath)]; 115 | if (currentlyActiveBlock) { 116 | updateGroupHeader(currentlyActiveBlock); 117 | } else { 118 | updateGroupHeader(firstBlock); // Fallback to first block if no active block found 119 | } 120 | 121 | this.addHeaderClickHandler(header, tabsContainer, group); 122 | 123 | if (firstBlock && firstBlock.parentElement) { 124 | firstBlock.parentElement.prepend(header); 125 | this.addHeaderHoverEffect(header, group, groupButtonsContainer); 126 | } 127 | }// processGroup 128 | 129 | private updateHeaderLanguageClasses(container: HTMLElement, language: string) { 130 | this.removeLanguageClasses(container); 131 | container.classList.add(`codeblock-customizer-language-` + (language.length > 0 ? language.toLowerCase() : "nolang")); 132 | 133 | const borderColor = getBorderColorByLanguage(language, getPropertyFromLanguageSpecificColors("codeblock.borderColor", this.plugin.settings)); 134 | if (borderColor.length > 0) { 135 | container.classList.add(`hasLangBorderColor`); 136 | } else { 137 | container.classList.remove(`hasLangBorderColor`); 138 | } 139 | }// updateHeaderLanguageClasses 140 | 141 | private updateHeaderLanguageSpecificClasses(container: HTMLElement, language: string) { 142 | this.removeLanguageSpecificClasses(container); 143 | const codeblockLanguageSpecificClass = getLanguageSpecificColorClass(language, this.plugin.settings.SelectedTheme.colors[getCurrentMode()].languageSpecificColors); 144 | if (codeblockLanguageSpecificClass) { 145 | container.classList.add(codeblockLanguageSpecificClass); 146 | } 147 | }// updateHeaderLanguageSpecificClasses 148 | 149 | private updateHeaderLanguageIcon(container: HTMLElement, iconElement: HTMLElement, displayLanguage: string): HTMLElement { 150 | if (iconElement && iconElement.parentNode) { 151 | iconElement.parentNode.removeChild(iconElement); 152 | } 153 | 154 | let newIconElement: HTMLElement; 155 | if (displayLanguage) { 156 | const Icon = getLanguageIcon(displayLanguage); 157 | newIconElement = Icon ? createCodeblockIcon(displayLanguage) : createCodeblockIcon("NoIcon"); 158 | } else { 159 | newIconElement = createCodeblockIcon("NoIcon"); 160 | } 161 | 162 | container.prepend(newIconElement); 163 | return newIconElement; 164 | }// updateHeaderLanguageIcon 165 | 166 | private updateHeaderButtons(buttonsContainer: HTMLElement, parameters: Parameters, blockElement: HTMLPreElement) { 167 | buttonsContainer.empty(); 168 | const tempButtonsContainer = createButtons(parameters, blockElement); 169 | while (tempButtonsContainer.firstChild) { 170 | buttonsContainer.appendChild(tempButtonsContainer.firstChild); 171 | } 172 | }// updateHeaderButtons 173 | 174 | private updateHeaderCollapseIcon(collapseIcon: HTMLElement | null, header: HTMLElement, currentBlock: HTMLPreElement, parameters: Parameters): HTMLElement | null { 175 | if (collapseIcon && collapseIcon.parentElement === header) { 176 | header.removeChild(collapseIcon); 177 | } 178 | 179 | const disableFoldUnlessSpecified = this.plugin.settings.SelectedTheme.settings.header.disableFoldUnlessSpecified; 180 | const inverseFold = this.plugin.settings.SelectedTheme.settings.codeblock.inverseFold; 181 | const isCollapseEnabled = !((disableFoldUnlessSpecified && !inverseFold && !parameters.fold) || (disableFoldUnlessSpecified && inverseFold && !parameters.unfold)); 182 | let newCollapseIcon: HTMLElement | null = null; 183 | 184 | if (isCollapseEnabled) { 185 | const isCollapsed = currentBlock.classList.contains('codeblock-customizer-codeblock-collapsed') || currentBlock.classList.contains('codeblock-customizer-codeblock-semi-collapsed'); 186 | 187 | newCollapseIcon = createCodeblockCollapse(isCollapsed); 188 | header.appendChild(newCollapseIcon); 189 | header.classList.remove(`noCollapseIcon`); 190 | 191 | if (isCollapsed && currentBlock.classList.contains('codeblock-customizer-codeblock-collapsed')) { 192 | header.classList.add("collapsed"); 193 | } else { 194 | header.classList.remove("collapsed"); 195 | } 196 | } else { 197 | header.classList.add(`noCollapseIcon`); 198 | header.classList.remove("collapsed"); 199 | } 200 | 201 | return newCollapseIcon; 202 | }// updateHeaderCollapseIcon 203 | 204 | private updateHeaderFileName(fileNameElement: HTMLElement, headerDisplayText: string) { 205 | fileNameElement.empty(); 206 | fileNameElement.setText(headerDisplayText); 207 | }// updateHeaderFileName 208 | 209 | private addHeaderClickHandler(headerContainer: HTMLElement, tabsContainer: HTMLElement, group: HTMLPreElement[]) { 210 | const headerClickHandler = (event: MouseEvent) => { 211 | if (!tabsContainer.contains(event.target as Node)) { 212 | const activeBlock = group.find(block => block.style.display !== 'none'); 213 | if (!activeBlock) 214 | return; 215 | 216 | this.foldCodeBlcok(activeBlock, headerContainer); 217 | } 218 | }; 219 | headerContainer.addEventListener('click', headerClickHandler); 220 | this.clickListeners.push(() => headerContainer.removeEventListener('click', headerClickHandler)); 221 | }// addHeaderClickHandler 222 | 223 | private addHeaderHoverEffect(headerContainer: HTMLElement, groupedBlocks: HTMLPreElement[], buttonsContainer: HTMLElement) { 224 | buttonsContainer.classList.add("hidden"); 225 | 226 | const mouseEnterHandler = () => { 227 | buttonsContainer.classList.remove("hidden"); 228 | }; 229 | 230 | const mouseLeaveHandler = () => { 231 | buttonsContainer.classList.add("hidden"); 232 | }; 233 | 234 | const elementsToHover = [headerContainer, ...groupedBlocks]; 235 | 236 | elementsToHover.forEach(element => { 237 | element.addEventListener('mouseenter', mouseEnterHandler); 238 | element.addEventListener('mouseleave', mouseLeaveHandler); 239 | this.hoverListeners.push(() => { 240 | element.removeEventListener('mouseenter', mouseEnterHandler); 241 | element.removeEventListener('mouseleave', mouseLeaveHandler); 242 | }); 243 | }); 244 | }// addHeaderHoverEffect 245 | 246 | private hideGroupedCodeBlocks(group: HTMLPreElement[]) { 247 | group.forEach(blockElement => { 248 | const existingHeader = blockElement.querySelector('.codeblock-customizer-header-container-specific'); 249 | if (existingHeader) { 250 | existingHeader.remove(); 251 | } 252 | blockElement.style.display = 'none'; 253 | blockElement.classList.add('displayedInGroup'); 254 | }); 255 | }// hideGroupedCodeBlocks 256 | 257 | private createHeader(params: Parameters, groupName: string): HTMLElement { 258 | const codeblockLanguageSpecificClass = getLanguageSpecificColorClass(params.language, this.plugin.settings.SelectedTheme.colors[getCurrentMode()].languageSpecificColors); 259 | const container = createContainer(params.specificHeader, params.language, false, codeblockLanguageSpecificClass, 'codeblock-customizer-header-group-container'); 260 | container.setAttribute("group", groupName); 261 | return container; 262 | }// createHeader 263 | 264 | private setupMutationObserver(attributes: string[]) { 265 | if (this.observer) { 266 | this.observer.disconnect(); 267 | } 268 | 269 | this.observer = new MutationObserver((mutations) => { 270 | let process = false; 271 | for (const mutation of mutations) { 272 | // child list changes (addition/removal of
 or other elements)
273 |         if (mutation.type === 'childList') {
274 |           const targetEl = mutation.target as HTMLElement;
275 |           if (targetEl.tagName === 'PRE' || targetEl.querySelector('pre.codeblock-customizer-grouped')) {
276 |             process = true;
277 |             break;
278 |           }
279 |         }
280 |         // attribute changes, specifically for 'groupname'
281 |         else if (mutation.type === 'attributes' && mutation.attributeName === 'groupname') {
282 |           process = true;
283 |           break;
284 |         }
285 |         // 'class' attribute change on a 
 element, but ONLY if it adds/removes the 'codeblock-customizer-grouped' class
286 |         else if (mutation.type === 'attributes' && mutation.attributeName === 'class' && mutation.target instanceof HTMLPreElement) {
287 |           const oldClassList = new Set((mutation.oldValue || '').split(' '));
288 |           const newClassList = new Set((mutation.target.getAttribute('class') || '').split(' '));
289 | 
290 |           const wasGrouped = oldClassList.has('codeblock-customizer-grouped');
291 |           const isGrouped = newClassList.has('codeblock-customizer-grouped');
292 | 
293 |           if (wasGrouped !== isGrouped) { // if a block became grouped or stopped being grouped
294 |             process = true;
295 |             break;
296 |           }
297 |         }
298 |       }
299 | 
300 |       if (process) {
301 |         this.debouncedProcess();
302 |       }
303 |     });
304 | 
305 |     this.observer.observe(this.containerEl, {
306 |       childList: true,
307 |       subtree: true,
308 |       attributes: true,
309 |       attributeFilter: attributes,
310 |       attributeOldValue: true
311 |     });
312 |   }// setupMutationObserver
313 | 
314 |   private disconnectObserver() {
315 |     if (this.observer) {
316 |       this.observer.disconnect();
317 |     }
318 |   }// disconnectObserver
319 | 
320 |   private reconnectObserver(attributes: string[]) {
321 |     if (this.observer) {
322 |       this.observer.observe(this.containerEl, {
323 |         childList: true, subtree: true, attributes: true, attributeFilter: attributes, attributeOldValue: true
324 |       });
325 |     }
326 |   }// reconnectObserver
327 | 
328 |   private cleanupListeners(){
329 |     this.clickListeners.forEach(removeListener => removeListener());
330 |     this.clickListeners = [];
331 |     this.hoverListeners.forEach(removeListener => removeListener());
332 |     this.hoverListeners = [];
333 |   }//cleanupListeners
334 | 
335 |   private removeLanguageClasses(element: HTMLElement) {
336 |     const regex = /\bcodeblock-customizer-language-[^\s]+\b/g;
337 | 
338 |     if (element && element.className) {
339 |       element.className = element.className.replace(regex, '').trim();
340 |     }
341 |   }// removeLanguageClasses
342 | 
343 |   private removeLanguageSpecificClasses(element: HTMLElement) {
344 |     const regex = /\bcodeblock-customizer-languageSpecific-[^\s]+\b/g;
345 | 
346 |     if (element && element.className) {
347 |       element.className = element.className.replace(regex, '').trim();
348 |     }
349 |   }// removeLanguageSpecificClasses
350 | 
351 |   private getConsecutiveGroups(allCodeBlockContainers: NodeListOf): HTMLPreElement[][] {
352 |     const distinctConsecutiveGroups: HTMLPreElement[][] = [];
353 |     let currentConsecutiveGroup: HTMLPreElement[] = [];
354 | 
355 |     const containerArray = Array.from(allCodeBlockContainers);
356 | 
357 |     for (let i = 0; i < containerArray.length; i++) {
358 |       const currentContainer = containerArray[i];
359 |       const currentPreElement = currentContainer.querySelector('pre.codeblock-customizer-grouped') as HTMLPreElement | null;
360 | 
361 |       if (!currentPreElement) {
362 |         if (currentConsecutiveGroup.length > 0) {
363 |           distinctConsecutiveGroups.push(currentConsecutiveGroup);
364 |           currentConsecutiveGroup = [];
365 |         }
366 |         continue;
367 |       }
368 | 
369 |       const currentGroupName = currentPreElement.getAttribute('groupname');
370 | 
371 |       if (currentGroupName) {
372 |         if (currentConsecutiveGroup.length === 0) {
373 |           currentConsecutiveGroup.push(currentPreElement);
374 |         } else {
375 |             const lastContainerInGroup = currentConsecutiveGroup[currentConsecutiveGroup.length - 1].closest('.el-pre.codeblock-customizer-pre-parent');
376 |             if (!lastContainerInGroup) {
377 |               if (currentConsecutiveGroup.length > 0) {
378 |                 distinctConsecutiveGroups.push(currentConsecutiveGroup);
379 |               }
380 |               currentConsecutiveGroup = [currentPreElement];
381 |               continue;
382 |             }
383 | 
384 |             let nextNode: ChildNode | null = lastContainerInGroup.nextSibling;
385 |             let foundDirectConsecutiveContainer = false;
386 | 
387 |             while (nextNode) {
388 |               if (nextNode === currentContainer) {
389 |                 foundDirectConsecutiveContainer = true;
390 |                 break;
391 |               }
392 | 
393 |               if (nextNode.nodeType === Node.ELEMENT_NODE) {
394 |                 break;
395 |               }
396 | 
397 |               if (nextNode.nodeType === Node.TEXT_NODE && nextNode.textContent && nextNode.textContent.trim().length > 0) {
398 |                 break;
399 |               }
400 | 
401 |               nextNode = nextNode.nextSibling;
402 |             }
403 | 
404 |           if (foundDirectConsecutiveContainer && currentGroupName === currentConsecutiveGroup[0].getAttribute('groupname')) {
405 |             currentConsecutiveGroup.push(currentPreElement);
406 |           } else {
407 |             distinctConsecutiveGroups.push(currentConsecutiveGroup);
408 |             currentConsecutiveGroup = [currentPreElement];
409 |           }
410 |         }
411 |       } else {
412 |         if (currentConsecutiveGroup.length > 0) {
413 |           distinctConsecutiveGroups.push(currentConsecutiveGroup);
414 |           currentConsecutiveGroup = [];
415 |         }
416 |       }
417 |     }
418 | 
419 |     if (currentConsecutiveGroup.length > 0) {
420 |       distinctConsecutiveGroups.push(currentConsecutiveGroup);
421 |     }
422 | 
423 |     return distinctConsecutiveGroups;
424 |   }// getConsecutiveGroups
425 | 
426 |   private getStoredTabIndex(groupName: string, documentPath: string): number {
427 |     const documentState = this.activeReadingViewTabs.get(documentPath);
428 |     if (documentState) {
429 |       const storedIndex = documentState.get(groupName);
430 |       if (storedIndex !== undefined) {
431 |         return storedIndex;
432 |       }
433 |     }
434 |     return 0; // default to the first tab
435 |   }/// getStoredTabIndex
436 | 
437 |   private setStoredTabIndex(groupName: string, documentPath: string, index: number) {
438 |     let documentState = this.activeReadingViewTabs.get(documentPath);
439 |     if (!documentState) {
440 |       documentState = new Map();
441 |       this.activeReadingViewTabs.set(documentPath, documentState);
442 |     }
443 |     documentState.set(groupName, index);
444 |   }// setStoredTabIndex
445 | 
446 |   private addTabs(frag: DocumentFragment, groupMembers: HTMLPreElement[], updateGroupHeader: (currentBlock: HTMLPreElement, tabsContainer: HTMLElement) => void, groupName: string, documentPath: string): HTMLElement {
447 |     const tabsContainer = document.createElement('div');
448 |     tabsContainer.classList.add('codeblock-customizer-header-group-tabs');
449 | 
450 |     let activeTabIndex = this.getStoredTabIndex(groupName, documentPath);
451 |     // Ensure the stored index is within bounds
452 |     if (activeTabIndex >= groupMembers.length) {
453 |       activeTabIndex = 0;
454 |     }
455 | 
456 |     const activeBlock = groupMembers[activeTabIndex];
457 | 
458 |     groupMembers.forEach((blockElement, index) => {
459 |       const parameters = this.getParametersFromElement(blockElement);
460 |       const displayLangName = getDisplayLanguageName(parameters.language);
461 |       const tabText = parameters.tab || displayLangName || `Tab ${index + 1}`;
462 | 
463 |       const tab = createCodeblockLang(parameters.language, `codeblock-customizer-header-group-tab`, tabText);
464 |       tab.setAttribute('data-codeblock-target-index', index.toString());
465 | 
466 |       if (blockElement === activeBlock) {
467 |         tab.classList.add('active');
468 |         blockElement.style.display = '';
469 |       } else {
470 |         tab.classList.remove('active');
471 |         blockElement.style.display = 'none';
472 |       }
473 |       
474 |       tabsContainer.appendChild(tab);
475 |     });
476 | 
477 |     this.addTabClickHandler(tabsContainer, groupMembers, updateGroupHeader, groupName, documentPath);
478 | 
479 |     frag.appendChild(tabsContainer);
480 |     return tabsContainer;
481 |   }// addTabs
482 | 
483 |   private addTabClickHandler(tabsContainer: HTMLElement, groupMembers: HTMLPreElement[], updateGroupHeader: (currentBlock: HTMLPreElement, tabsContainer: HTMLElement) => void, groupName: string, documentPath: string) {
484 |     const tabClickHandler = (event: MouseEvent) => {
485 |       const clickedTab = (event.target as HTMLElement).closest('.codeblock-customizer-header-group-tab') as HTMLElement | null;
486 |       if (!clickedTab) 
487 |         return;
488 | 
489 |       const targetIndex = parseInt(clickedTab.getAttribute('data-codeblock-target-index') || '0', 10);
490 |       const blockElement = groupMembers[targetIndex];
491 |       if (!blockElement) 
492 |         return;
493 | 
494 |       const isActive = clickedTab.classList.contains('active');
495 |       if (isActive) {
496 |         // if the clicked tab is already active, only fold/unfold
497 |         const mainHeader = tabsContainer.parentElement;
498 |         if (mainHeader) {
499 |           this.foldCodeBlcok(blockElement, mainHeader);
500 |         }
501 |       } else {
502 |         // if a different tab is clicked, hide all blocks and show that block
503 |         this.switchTab(clickedTab, blockElement, groupMembers, tabsContainer, updateGroupHeader);
504 |         this.setStoredTabIndex(groupName, documentPath, targetIndex);
505 |       }
506 |     };
507 | 
508 |     tabsContainer.addEventListener('click', tabClickHandler);
509 |     this.clickListeners.push(() => tabsContainer.removeEventListener('click', tabClickHandler));
510 |   }// addTabClickHandler
511 | 
512 |   private foldCodeBlcok(activeBlock: HTMLPreElement, header: HTMLElement) {
513 |     const lines = activeBlock.querySelectorAll('code > div');
514 |     const codeblockLineCount = lines.length;
515 |     const semiFold = this.plugin.settings.SelectedTheme.settings.semiFold.enableSemiFold;
516 |     const visibleLines = this.plugin.settings.SelectedTheme.settings.semiFold.visibleLines;
517 | 
518 |     const currentCollapseIcon = header.querySelector('.codeblock-customizer-header-collapse') as HTMLElement | null;
519 |     if (!currentCollapseIcon)
520 |       return;
521 | 
522 |     if (semiFold && codeblockLineCount >= visibleLines + fadeOutLineCount) {
523 |       toggleFold(activeBlock, currentCollapseIcon, `codeblock-customizer-codeblock-semi-collapsed`);
524 |     } else {
525 |       toggleFold(activeBlock, currentCollapseIcon, `codeblock-customizer-codeblock-collapsed`);
526 |       if (header)
527 |         header.classList.toggle("collapsed");
528 |     }
529 |   }// foldCodeBlcok
530 | 
531 |   private switchTab(clickedTab: HTMLElement, targetBlock: HTMLPreElement, allGroupBlocks: HTMLPreElement[], tabsContainer: HTMLElement, updateHeaderCallback: (currentBlock: HTMLPreElement, tabsContainer: HTMLElement) => void) {
532 |     allGroupBlocks.forEach(b => b.style.display = 'none');
533 |     targetBlock.style.display = '';
534 | 
535 |     tabsContainer.querySelectorAll('.codeblock-customizer-header-group-tab').forEach(btn => btn.classList.remove('active'));
536 |     clickedTab.classList.add('active');
537 | 
538 |     updateHeaderCallback(targetBlock, tabsContainer);
539 |   }// switchTab
540 | 
541 |   private getParametersFromElement(element: HTMLElement): Parameters {
542 |     const paramsJson = element.dataset.parameters;
543 |     if (paramsJson) {
544 |       try {
545 |         return JSON.parse(paramsJson);
546 |       } catch (e) {
547 |         //console.error("Failed to parse parameters from element:", element, e);
548 |         return getDefaultParameters();
549 |       }
550 |     }
551 |     return getDefaultParameters();
552 |   }// getParametersFromElement
553 | }// GroupedCodeBlockRenderChild
554 | 
555 | function debounce void>(func: T, wait: number, immediate: boolean) {
556 |   let timeout: NodeJS.Timeout | null;
557 |   let result: ReturnType | undefined;
558 | 
559 |   return function(this: any, ...args: any[]) {
560 |     const later = function() {
561 |       timeout = null;
562 |       if (!immediate) {
563 |         result = func.apply(this, args);
564 |       }
565 |     };
566 | 
567 |     const callNow = immediate && !timeout;
568 |     clearTimeout(timeout as NodeJS.Timeout);
569 |     timeout = setTimeout(later, wait);
570 |     if (callNow) {
571 |       result = func.apply(this, args);
572 |     }
573 |     return result;
574 |   } as T;
575 | }// debounce
576 | 


--------------------------------------------------------------------------------
/src/ReadingView.ts:
--------------------------------------------------------------------------------
   1 | import { MarkdownView, MarkdownPostProcessorContext, sanitizeHTMLToDom, setIcon, MarkdownSectionInformation, MarkdownRenderer, loadPrism, Notice } from "obsidian";
   2 | 
   3 | import { getLanguageIcon, createContainer, createCodeblockLang, createCodeblockIcon, createFileName, createCodeblockCollapse, getCurrentMode, getBorderColorByLanguage, removeCharFromStart, createUncollapseCodeButton, addTextToClipboard, getLanguageSpecificColorClass, findAllOccurrences, Parameters, getAllParameters, getPropertyFromLanguageSpecificColors, getLanguageConfig, getFileCacheAndContentLines, PromptEnvironment, getPWD, createPromptContext, PromptCache, renderPromptLine, computePromptLines } from "./Utils";
   4 | import CodeBlockCustomizerPlugin from "./main";
   5 | import { CodeblockCustomizerSettings, ThemeSettings } from "./Settings";
   6 | import { fadeOutLineCount } from "./Const";
   7 | 
   8 | import { visitParents } from "unist-util-visit-parents";
   9 | import { fromHtml } from "hast-util-from-html";
  10 | import { toHtml } from "hast-util-to-html";
  11 | 
  12 | interface IndentationInfo {
  13 |   indentationLevels: number;
  14 |   insertCollapse: boolean;
  15 | }
  16 | 
  17 | export async function ReadingView(codeBlockElement: HTMLElement, context: MarkdownPostProcessorContext, plugin: CodeBlockCustomizerPlugin) {
  18 |   const codeElm: HTMLElement | null = codeBlockElement.querySelector('pre > code');
  19 |   if (!codeElm) 
  20 |     return;
  21 | 
  22 |   /*if (Array.from(codeElm.classList).some(className => /^language-\S+/.test(className)))
  23 |   while(!codeElm.classList.contains("is-loaded"))
  24 |     await sleep(2);*/
  25 | 
  26 |   const preElements: Array = await getPreElements(codeBlockElement);
  27 |   if (!preElements)
  28 |     return;
  29 | 
  30 |   const codeBlockSectionInfo = context.getSectionInfo(codeElm);
  31 |   if (!codeBlockSectionInfo) {
  32 |     // PDF export
  33 |     let id: string | null = null;
  34 |     if (codeBlockElement.parentElement?.classList.contains("internal-embed")) {
  35 |       const src = codeBlockElement.parentElement?.getAttribute("src");
  36 |       if (src) {
  37 |         const indexOfCaret = src.indexOf("^");
  38 |         if (indexOfCaret !== -1) {
  39 |           id = src.substring(indexOfCaret + 1);
  40 |         }
  41 |       }
  42 |     }
  43 |     handlePDFExport(preElements, context, plugin, id);
  44 |   }
  45 | 
  46 |   const sectionInfo: MarkdownSectionInformation | null = context.getSectionInfo(preElements[0]);
  47 |   if (!sectionInfo)
  48 |     return;
  49 | 
  50 |   const codeblockLines = Array.from({length: sectionInfo.lineEnd - sectionInfo.lineStart + 1}, (_,number) => number + sectionInfo.lineStart).map((lineNumber) => sectionInfo.text.split('\n')[lineNumber]);
  51 |   const codeLines = Array.from(codeblockLines);
  52 |   if (codeLines.length >= 2) {
  53 |     codeLines.shift();
  54 |     codeLines.pop();
  55 |   }
  56 |   const indentationLevels = trackIndentation(codeLines);
  57 |   const codeBlockFirstLines = getCodeBlocksFirstLines(codeblockLines);
  58 | 
  59 |   await processCodeBlockFirstLines(preElements, codeBlockFirstLines, indentationLevels, codeblockLines, context.sourcePath, plugin);
  60 | }// ReadingView
  61 | 
  62 | async function addCustomSyntaxHighlight(codeblockLines: string[], language: string) {
  63 |   if (codeblockLines.length > 1) {
  64 |     codeblockLines = codeblockLines.slice(1);
  65 |   } else {
  66 |     codeblockLines = [];
  67 |   }
  68 | 
  69 |   if (codeblockLines.length === 0)
  70 |     return "";
  71 | 
  72 |   const prism = await loadPrism();
  73 |   const langDefinition = prism.languages[language];
  74 | 
  75 |   const html = await prism.highlight(codeblockLines.join('\n'), langDefinition, language);
  76 | 
  77 |   return html || "";
  78 | }// addCustomSyntaxHighlight
  79 | 
  80 | async function getPreElements(element: HTMLElement) {
  81 |   const preElements: Array = Array.from(element.querySelectorAll("pre:not(.frontmatter)"));
  82 |   return preElements;
  83 | }// getPreElements
  84 | 
  85 | function trackIndentation(lines: string[]): IndentationInfo[] {
  86 |   const result: IndentationInfo[] = [];
  87 |   const spaceIndentRegex = /^( {0,4}|\t)*/;
  88 | 
  89 |   for (let i = 0; i < lines.length; i++) {
  90 |     const line = lines[i] ?? "";
  91 |     const match = line.match(spaceIndentRegex);
  92 |     let currentIndentLevel = 0;
  93 | 
  94 |     if (match) {
  95 |       const indentation = match[0];
  96 |       currentIndentLevel = calculateIndentLevel(indentation);
  97 |     }
  98 | 
  99 |     const nextLine = lines[i + 1] ?? "";
 100 |     let nextIndentLevel = 0;
 101 | 
 102 |     if (nextLine) {
 103 |       const nextMatch = nextLine.match(spaceIndentRegex);
 104 | 
 105 |       if (nextMatch) {
 106 |         const nextIndentation = nextMatch[0];
 107 |         nextIndentLevel = calculateIndentLevel(nextIndentation);
 108 |       }
 109 |     }
 110 | 
 111 |     const info: IndentationInfo = {
 112 |       indentationLevels: currentIndentLevel,
 113 |       insertCollapse: nextIndentLevel > currentIndentLevel,
 114 |     };
 115 | 
 116 |     result.push(info);
 117 |   }
 118 | 
 119 |   return result;
 120 | }// trackIndentation
 121 | 
 122 | function calculateIndentLevel(indentation: string): number {
 123 |   let indentLevel = 0;
 124 |   let spaceCount = 0;
 125 | 
 126 |   for (const char of indentation) {
 127 |     if (char === '\t') {
 128 |       indentLevel += 1;
 129 |       spaceCount = 0;
 130 |     } else if (char === ' ') {
 131 |       spaceCount += 1;
 132 |       if (spaceCount === 4) {
 133 |         indentLevel += 1;
 134 |         spaceCount = 0;
 135 |       }
 136 |     }
 137 |   }
 138 | 
 139 |   // Handle remaining spaces less than 4
 140 |   if (spaceCount > 0) {
 141 |     indentLevel += 1;
 142 |   }
 143 | 
 144 |   return indentLevel;
 145 | }// calculateIndentLevel
 146 | 
 147 | export async function calloutPostProcessor(codeBlockElement: HTMLElement, context: MarkdownPostProcessorContext, plugin: CodeBlockCustomizerPlugin) {
 148 |   const callouts: HTMLElement | null = codeBlockElement.querySelector('.callout');
 149 |   if (!callouts) 
 150 |     return;
 151 | 
 152 |   const calloutPreElements: Array = Array.from(callouts.querySelectorAll('pre:not(.frontmatter)'));
 153 |   if (!calloutPreElements)
 154 |     return;
 155 | 
 156 |   const markdownView = plugin.app.workspace.getActiveViewOfType(MarkdownView);
 157 |   const viewMode = markdownView?.getMode();
 158 | 
 159 |   if (viewMode === "source") {
 160 |     const foundCmView = await waitForCmView(context);
 161 |     if (!foundCmView)
 162 |       return;
 163 | 
 164 |     // @ts-ignore
 165 |     const calloutText = context?.containerEl?.cmView?.widget?.text?.split("\n") || null;
 166 |     let codeBlockFirstLines: string[] = [];
 167 |     codeBlockFirstLines = getCallouts(calloutText);
 168 |     await processCodeBlockFirstLines(calloutPreElements, codeBlockFirstLines, null, [], context.sourcePath, plugin);
 169 |   }
 170 | }// calloutPostProcessor
 171 | 
 172 | async function waitForCmView(context: MarkdownPostProcessorContext, maxRetries = 25, delay = 2): Promise {
 173 |   // @ts-ignore
 174 |   if (context?.containerEl?.cmView)
 175 |     return true;
 176 | 
 177 |   let retries = 0;
 178 |   // @ts-ignore
 179 |   while (!context?.containerEl?.cmView) {
 180 |     if (retries >= maxRetries) {
 181 |       return false;
 182 |     }
 183 |     retries++;
 184 |     await sleep(delay);
 185 |   }
 186 |   return true;
 187 | }// waitForCmView
 188 | 
 189 | async function checkCustomSyntaxHighlight(parameters: Parameters, codeblockLines: string[], preCodeElm: HTMLElement, plugin: CodeBlockCustomizerPlugin ){
 190 |   const customLangConfig = getLanguageConfig(parameters.language, plugin);
 191 |   const customFormat = customLangConfig?.format ?? undefined;
 192 |   if (customFormat){
 193 |     const highlightedLines = await addCustomSyntaxHighlight(codeblockLines, customFormat);
 194 |     if (highlightedLines.length > 0){
 195 |       preCodeElm.innerHTML = highlightedLines;
 196 |     }
 197 |   }
 198 | }// checkCustomSyntaxHighlight
 199 | 
 200 | async function processCodeBlockFirstLines(preElements: HTMLElement[], codeBlockFirstLines: string[], indentationLevels: IndentationInfo[] | null, codeblockLines: string[], sourcepath: string, plugin: CodeBlockCustomizerPlugin ) {
 201 |   if (preElements.length !== codeBlockFirstLines.length)
 202 |     return;
 203 | 
 204 |   for (const [key, preElement] of preElements.entries()) {
 205 |     const codeBlockFirstLine = codeBlockFirstLines[key];
 206 |     const preCodeElm = preElement.querySelector('pre > code');
 207 | 
 208 |     if (!preCodeElm)
 209 |       return;
 210 | 
 211 |     if (preCodeElm.querySelector("code [class*='codeblock-customizer-line']"))
 212 |       continue;
 213 | 
 214 |     if (Array.from(preCodeElm.classList).some(className => /^language-\S+/.test(className)))
 215 |       while(!preCodeElm.classList.contains("is-loaded"))
 216 |         await sleep(2);
 217 | 
 218 |     const parameters = getAllParameters(codeBlockFirstLine, plugin.settings);
 219 |     if (parameters.exclude)
 220 |       continue;
 221 | 
 222 |     if (parameters.group && parameters.group.length > 0) {
 223 |       preElement.setAttribute('groupname', parameters.group);
 224 |       preElement.setAttribute('sourcepath', sourcepath);
 225 |       const paramsJsonString = JSON.stringify(parameters);
 226 |       preElement.dataset.parameters = paramsJsonString;
 227 |       preElement.classList.add('codeblock-customizer-grouped');
 228 |     }
 229 | 
 230 |     await checkCustomSyntaxHighlight(parameters, codeblockLines, preCodeElm as HTMLElement, plugin);
 231 | 
 232 |     const codeblockLanguageSpecificClass = getLanguageSpecificColorClass(parameters.language, plugin.settings.SelectedTheme.colors[getCurrentMode()].languageSpecificColors);
 233 |     await addClasses(preElement, parameters, plugin, preCodeElm as HTMLElement, indentationLevels, codeblockLanguageSpecificClass, sourcepath);
 234 |   }
 235 | }// processCodeBlockFirstLines
 236 | 
 237 | async function addClasses(preElement: HTMLElement, parameters: Parameters, plugin: CodeBlockCustomizerPlugin, preCodeElm: HTMLElement, indentationLevels: IndentationInfo[] | null, codeblockLanguageSpecificClass: string, sourcePath: string) {
 238 |   const frag = document.createDocumentFragment();
 239 |   
 240 |   preElement.classList.add(`codeblock-customizer-pre`);  
 241 |   preElement.classList.add(`codeblock-customizer-language-` + (parameters.language.length > 0 ? parameters.language.toLowerCase() : "nolang"));
 242 | 
 243 |   if (codeblockLanguageSpecificClass)
 244 |     preElement.classList.add(codeblockLanguageSpecificClass);
 245 | 
 246 |   if (preElement.parentElement)
 247 |     preElement.parentElement.classList.add(`codeblock-customizer-pre-parent`);
 248 | 
 249 |   const buttons = createButtons(parameters);
 250 |   frag.appendChild(buttons);
 251 | 
 252 |   const header = HeaderWidget(preElement as HTMLPreElement, parameters, plugin.settings, sourcePath, plugin);
 253 |   frag.insertBefore(header, frag.firstChild);
 254 | 	
 255 |   preElement.insertBefore(frag, preElement.firstChild);
 256 | 
 257 |   const lines = Array.from(preCodeElm.innerHTML.split('\n')) || 0;
 258 |   if (parameters.fold) {
 259 |     toggleFoldClasses(preElement as HTMLPreElement, lines.length - 1, parameters.fold, plugin.settings.SelectedTheme.settings.semiFold.enableSemiFold, plugin.settings.SelectedTheme.settings.semiFold.visibleLines);
 260 |   }/* else {
 261 |     isFoldable(preElement as HTMLPreElement, lines.length - 1, plugin.settings.SelectedTheme.settings.semiFold.enableSemiFold, plugin.settings.SelectedTheme.settings.semiFold.visibleLines);
 262 |   }*/
 263 | 	
 264 |   const borderColor = getBorderColorByLanguage(parameters.language, getPropertyFromLanguageSpecificColors("codeblock.borderColor", plugin.settings));
 265 |   if (borderColor.length > 0)
 266 |     preElement.classList.add(`hasLangBorderColor`);
 267 | 
 268 |   await highlightLines(preCodeElm, parameters, plugin.settings.SelectedTheme.settings, indentationLevels, sourcePath, plugin);
 269 | }// addClasses
 270 | 
 271 | function createCopyButton(displayLanguage: string) {
 272 |   const container = document.createElement("button");
 273 |   container.classList.add(`codeblock-customizer-copy-code-button`);
 274 |   container.setAttribute("aria-label", "Copy code");
 275 | 
 276 |   if (displayLanguage) {
 277 |     if (displayLanguage)
 278 |       container.setText(displayLanguage);
 279 |     else
 280 |       setIcon(container, "copy");
 281 |   } else
 282 |     setIcon(container, "copy");
 283 | 
 284 |   return container;
 285 | }// createCopyButton
 286 | 
 287 | export function createButtons(parameters: Parameters, targetPreElement?: HTMLElement){
 288 |   const container = createDiv({cls: `codeblock-customizer-button-container`});
 289 |   const frag = document.createDocumentFragment();
 290 | 
 291 |   const copyButton = createCopyButton(parameters.displayLanguage);
 292 |     copyButton.addEventListener("click", (event) => {
 293 |     const preEl = targetPreElement || (event.currentTarget as HTMLElement).parentNode?.parentNode as HTMLElement;
 294 |     if (preEl) {
 295 |       copyCode(preEl, event);
 296 |     }
 297 |   });
 298 |   frag.appendChild(copyButton);
 299 | 
 300 |   const wrapCodeButton = createWrapCodeButton();
 301 |     wrapCodeButton.addEventListener("click", (event) => {
 302 |     const preEl = targetPreElement || (event.currentTarget as HTMLElement).parentNode?.parentNode as HTMLElement;
 303 |     if (preEl) {
 304 |       wrapCode(preEl, event);
 305 |     }
 306 |   });
 307 |   frag.appendChild(wrapCodeButton);
 308 | 
 309 |   container.appendChild(frag);
 310 |   return container;
 311 | }// createButtons
 312 | 
 313 | function createWrapCodeButton() {
 314 |   const container = document.createElement("button");
 315 |   container.classList.add(`codeblock-customizer-wrap-code`);
 316 |   container.setAttribute("aria-label", "Wrap/Unwrap code");
 317 |   setIcon(container, "wrap-text");
 318 | 
 319 |   return container;
 320 | }// createWrapCodeButton
 321 | 
 322 | function copyCode(preElement: HTMLElement, event: Event) {
 323 |   event.stopPropagation();
 324 | 
 325 |   if (!preElement)
 326 |     return;
 327 |   
 328 |   const lines = preElement.querySelectorAll("code");
 329 |   const codeTextArray: string[] = [];
 330 | 
 331 |   lines.forEach((line, index) => {
 332 |     //const codeElements = line.querySelectorAll('.codeblock-customizer-line-text');
 333 |     const codeElements = line.querySelectorAll('.codeblock-customizer-line-text:not(.codeblock-customizer-prompt-cmd-output)');
 334 |     codeElements.forEach((codeElement, codeIndex) => {
 335 |       const textContent = codeElement.textContent || "";
 336 |       codeTextArray.push(textContent);
 337 |       if (codeIndex !== codeElements.length - 1)
 338 |         codeTextArray.push('\n');
 339 |     });
 340 |   });
 341 | 
 342 |   const concatenatedCodeText = codeTextArray.join('');
 343 |   addTextToClipboard(concatenatedCodeText);
 344 | }// copyCode
 345 | 
 346 | function wrapCode(preElement: HTMLElement, event: Event) {
 347 |   event.stopPropagation();
 348 | 
 349 |   if (!preElement)
 350 |     return;
 351 | 
 352 |   const codeElement = preElement.querySelector('code');
 353 |   if (!codeElement)
 354 |     return;
 355 | 
 356 |   let wrapState = '';
 357 |   const currentWhiteSpace = window.getComputedStyle(codeElement).whiteSpace;
 358 |   if (currentWhiteSpace === 'pre') {
 359 |     wrapState = 'pre-wrap';
 360 |     new Notice("Code wrapped");
 361 |   } else {
 362 |     wrapState = 'pre';
 363 |     new Notice("Code unwrapped");
 364 |   }
 365 | 
 366 |   codeElement.style.setProperty("white-space", wrapState, "important");
 367 | 
 368 | }// wrapCode
 369 | 
 370 | async function handlePDFExport(preElements: Array, context: MarkdownPostProcessorContext, plugin: CodeBlockCustomizerPlugin, id: string | null) {
 371 |   const { cache, fileContentLines } = await getFileCacheAndContentLines(plugin, context.sourcePath);
 372 |   if (!cache || !fileContentLines)
 373 |     return;
 374 | 
 375 |   let codeBlockFirstLines: string[] = [];
 376 |   if (cache?.sections && !id) {
 377 |     codeBlockFirstLines = getCodeBlocksFirstLines(fileContentLines);
 378 |   } else if (cache?.blocks && id) { 
 379 |     codeBlockFirstLines = getCodeBlocksFirstLines(fileContentLines.slice(cache.blocks[id].position.start.line, cache.blocks[id].position.end.line));
 380 |   } else {
 381 |       console.error(`Metadata cache not found for file: ${context.sourcePath}`);
 382 |       return;
 383 |   }
 384 | 
 385 |   if (preElements.length !== codeBlockFirstLines.length)
 386 |     return;
 387 | 
 388 |   try {
 389 |     if (plugin.settings.SelectedTheme.settings.printing.enablePrintToPDFStyling)
 390 |       await PDFExport(preElements, plugin, codeBlockFirstLines, context.sourcePath);
 391 |   } catch (error) {
 392 |     console.error(`Error exporting to PDF: ${error.message}`);
 393 |     return;
 394 |   }
 395 |   return;
 396 | }// handlePDFExport
 397 | 
 398 | function HeaderWidget(preElements: HTMLPreElement, parameters: Parameters, settings: CodeblockCustomizerSettings, sourcePath: string, plugin: CodeBlockCustomizerPlugin) {
 399 |   const parent = preElements.parentNode;
 400 |   const codeblockLanguageSpecificClass = getLanguageSpecificColorClass(parameters.language, settings.SelectedTheme.colors[getCurrentMode()].languageSpecificColors);
 401 |   const container = createContainer(parameters.specificHeader, parameters.language, false, codeblockLanguageSpecificClass); // hasLangBorderColor must be always false in reading mode, because how the doc is generated
 402 |   const frag = document.createDocumentFragment();
 403 | 
 404 |   if (parameters.displayLanguage){
 405 |     const Icon = getLanguageIcon(parameters.displayLanguage)
 406 |     if (Icon) {
 407 |       frag.appendChild(createCodeblockIcon(parameters.displayLanguage));
 408 |     }
 409 |     frag.appendChild(createCodeblockLang(parameters.language));
 410 |   }
 411 |   frag.appendChild(createFileName(parameters.headerDisplayText, settings.SelectedTheme.settings.codeblock.enableLinks, sourcePath, plugin));
 412 | 
 413 |   const collapseEl = createCodeblockCollapse(parameters.fold);
 414 |   if ((plugin.settings.SelectedTheme.settings.header.disableFoldUnlessSpecified && !plugin.settings.SelectedTheme.settings.codeblock.inverseFold && !parameters.fold) ||
 415 |       (plugin.settings.SelectedTheme.settings.header.disableFoldUnlessSpecified && plugin.settings.SelectedTheme.settings.codeblock.inverseFold && !parameters.unfold)) {
 416 |     container.classList.add(`noCollapseIcon`);
 417 |   } else {
 418 |     frag.appendChild(collapseEl);
 419 |   }
 420 |   
 421 |   container.appendChild(frag);
 422 | 
 423 |   if (parent)
 424 |     parent.insertBefore(container, preElements);
 425 |   
 426 |   const semiFold = settings.SelectedTheme.settings.semiFold.enableSemiFold;
 427 |   const visibleLines = settings.SelectedTheme.settings.semiFold.visibleLines;
 428 | 
 429 |   // Add event listener to the widget element
 430 |   container.addEventListener("click", function() {
 431 |     //collapseEl.innerText = preElements.classList.contains(`codeblock-customizer-codeblock-collapsed`) ? "-" : "+";
 432 |     if ((plugin.settings.SelectedTheme.settings.header.disableFoldUnlessSpecified && !plugin.settings.SelectedTheme.settings.codeblock.inverseFold && !parameters.fold) ||
 433 |         (plugin.settings.SelectedTheme.settings.header.disableFoldUnlessSpecified && plugin.settings.SelectedTheme.settings.codeblock.inverseFold && !parameters.unfold)) {
 434 |       return;
 435 |     }
 436 |     if (semiFold) {
 437 |       const codeElements = preElements.getElementsByTagName("CODE");
 438 |       const lines = convertHTMLCollectionToArray(codeElements, true);
 439 |       if (lines.length >= visibleLines + fadeOutLineCount) {
 440 |         toggleFold(preElements, collapseEl, `codeblock-customizer-codeblock-semi-collapsed`);
 441 |       } else
 442 |         toggleFold(preElements, collapseEl, `codeblock-customizer-codeblock-collapsed`);
 443 |     } else {
 444 |       toggleFold(preElements, collapseEl, `codeblock-customizer-codeblock-collapsed`);
 445 |     }
 446 |   });
 447 |   
 448 |   if (parameters.fold) {
 449 |     if (semiFold) {
 450 |       const preCodeElm = preElements.querySelector("pre > code");
 451 |       let codeblockLineCount = 0;
 452 |       if (preCodeElm) {
 453 |         let codeblockLines = preCodeElm.innerHTML.split("\n");
 454 |         if (codeblockLines.length == 1)
 455 |           codeblockLines = ['',''];
 456 |         codeblockLineCount = codeblockLines.length - 1;
 457 |       }
 458 |       if (codeblockLineCount >= visibleLines + fadeOutLineCount) {
 459 |         preElements.classList.add(`codeblock-customizer-codeblock-semi-collapsed`);
 460 |       } else 
 461 |         preElements.classList.add(`codeblock-customizer-codeblock-collapsed`);
 462 |     }
 463 |     else
 464 |       preElements.classList.add(`codeblock-customizer-codeblock-collapsed`);
 465 |     preElements.classList.add(`codeblock-customizer-codeblock-default-collapse`);
 466 |   }
 467 | 
 468 |   return container
 469 | }// HeaderWidget
 470 | 
 471 | function createLineNumberElement(lineNumber: number, showNumbers: string) {
 472 |   const lineNumberWrapper = createDiv();
 473 |   if (showNumbers === "specific")
 474 |     lineNumberWrapper.classList.add(`codeblock-customizer-line-number-specific`);
 475 |   else if (showNumbers === "hide")
 476 |     lineNumberWrapper.classList.add(`codeblock-customizer-line-number-hide`);
 477 |   else 
 478 |     lineNumberWrapper.classList.add(`codeblock-customizer-line-number`);
 479 | 
 480 |   const lineNumberElement = createSpan({cls : `codeblock-customizer-line-number-element`});
 481 |   lineNumberElement.setText(lineNumber === -1 ? '' : lineNumber.toString());
 482 |   
 483 |   lineNumberWrapper.appendChild(lineNumberElement);
 484 | 
 485 |   return lineNumberWrapper;
 486 | }// createLineNumberElement
 487 | 
 488 | function createLineTextElement(line: string) {
 489 |   const lineText = line !== "" ? line : "
"; 490 | const sanitizedText = sanitizeHTMLToDom(lineText); 491 | const lineContentWrapper = createDiv({cls: `codeblock-customizer-line-text`, text: sanitizedText}); 492 | 493 | return lineContentWrapper; 494 | }// createLineTextElement 495 | 496 | function addIndentLine(inputString: string, insertCollapse = false): string { 497 | const indentRegex = /^(?:\t+|( {4})*)/; 498 | const match = inputString.match(indentRegex); 499 | const indent = match ? match[0] : ''; 500 | const isTabIndentation = /\t/.test(indent); 501 | const numIndentCharacters = isTabIndentation ? (indent.match(/\t/g) || []).length : (indent.match(/ {4}/g) || []).length; 502 | const indentSpan = createSpan({cls: "codeblock-customizer-indentation-guide", text: isTabIndentation ? "\t" : " "}); 503 | 504 | const spans = Array(numIndentCharacters).fill(indentSpan.outerHTML).join(''); 505 | const lastIndentPosition = isTabIndentation ? numIndentCharacters : numIndentCharacters * 4; 506 | const indicator = createSpan({cls: "codeblock-customizer-collapse-indicator"}); 507 | const iconSpan = createSpan({cls: "codeblock-customizer-collapse-icon"}); 508 | indicator.appendChild(iconSpan); 509 | 510 | let modifiedString = ""; 511 | if (insertCollapse) { 512 | modifiedString = inputString.slice(0, lastIndentPosition) + indicator.outerHTML + inputString.slice(lastIndentPosition); 513 | } 514 | 515 | const stringWithSpans = inputString.replace(indentRegex, spans); 516 | 517 | return insertCollapse ? modifiedString.replace(indentRegex, spans) : stringWithSpans; 518 | }// addIndentLine 519 | 520 | function extractLinesFromHTML(preCodeElm: HTMLElement): { htmlLines: string[]; textLines: string[] } { 521 | const tree = fromHtml(preCodeElm.innerHTML.replace(/\n/g, "
"), { fragment: true }); 522 | let htmlContent = preCodeElm.innerHTML; 523 | 524 | visitParents(tree, ["text", "element"], (node, parents) => { 525 | if (node.type === "element" && node.tagName === "br") { 526 | htmlContent = replaceNewlineWithBr(htmlContent, parents); 527 | } 528 | }); 529 | 530 | const splitTree = fromHtml(htmlContent); 531 | htmlContent = toHtml(splitTree); 532 | 533 | let htmlLines = htmlContent.split("
"); 534 | if (htmlLines.length === 1) 535 | htmlLines = ["", ""]; 536 | 537 | let textLines = preCodeElm.textContent?.split("\n") ?? []; 538 | if (textLines.length === 1 && htmlLines.length === 2 && htmlLines[0] === "" && htmlLines[1] === "") { 539 | textLines = ["", ""]; 540 | } 541 | 542 | preCodeElm.innerHTML = ""; 543 | 544 | return { htmlLines, textLines }; 545 | }// extractLinesFromHTML 546 | 547 | function replaceNewlineWithBr(htmlContent: string, parents: any[]): string { 548 | const brReplacement = parents.length >= 2 ? replaceWithNestedBr(parents) : "
"; 549 | return htmlContent.replace(/\n/, brReplacement); 550 | }// replaceNewlineWithBr 551 | 552 | function replaceWithNestedBr(parents: any[]): string { 553 | const nestedBr = parents.slice(1).reduce((ret: string, el) => { 554 | const clonedElement = structuredClone(el); 555 | clonedElement.children = []; 556 | const tags = toHtml(clonedElement).split(/(?<=>)(?=<\/)/); 557 | return tags.splice(-1) + ret + tags.join(""); 558 | }, "
"); 559 | return nestedBr; 560 | }// replaceWithNestedBr 561 | 562 | function isLineHighlighted(lineNumber: number, caseInsensitiveLineText: string, parameters: Parameters) { 563 | const result = { 564 | isHighlighted: false, 565 | color: '' 566 | }; 567 | 568 | // Highlight by line number hl:1,3-5 569 | const isHighlightedByLineNumber = parameters.defaultLinesToHighlight.lineNumbers.includes(lineNumber + parameters.lineNumberOffset); 570 | 571 | // Highlight every line which contains a specific word hl:test 572 | let isHighlightedByWord = false; 573 | const words = parameters.defaultLinesToHighlight.words; 574 | if (words.length > 0 && words.some(word => caseInsensitiveLineText.includes(word))) { 575 | isHighlightedByWord = true; 576 | } 577 | 578 | // Highlight specific lines if they contain the specified word hl:1|test,3-5|test 579 | let isHighlightedByLineSpecificWord = false; 580 | const lineSpecificWords = parameters.defaultLinesToHighlight.lineSpecificWords; 581 | if (lineSpecificWords.length > 0) { 582 | lineSpecificWords.forEach(lsWord => { 583 | if (lsWord.lineNumber === lineNumber && lsWord.words.some(word => caseInsensitiveLineText.includes(word))) { 584 | isHighlightedByLineSpecificWord = true; 585 | } 586 | }); 587 | } 588 | 589 | // Highlight line by line number imp:1,3-5 590 | const altHLMatch = parameters.alternativeLinesToHighlight.lines.filter((hl) => hl.lineNumbers.includes(lineNumber + parameters.lineNumberOffset)); 591 | 592 | // Highlight every line which contains a specific word imp:test 593 | let isAlternativeHighlightedByWord = false; 594 | let isAlternativeHighlightedByWordColor = ''; 595 | const altwords = parameters.alternativeLinesToHighlight.words; 596 | if (altwords.length > 0 && altwords.some(altwordObj => altwordObj.words.some(word => caseInsensitiveLineText.includes(word.toLowerCase())))) { 597 | altwords.forEach(altwordObj => { 598 | if (altwordObj.words.some(word => caseInsensitiveLineText.includes(word.toLowerCase()))) { 599 | isAlternativeHighlightedByWord = true; 600 | isAlternativeHighlightedByWordColor = altwordObj.colorName; 601 | } 602 | }); 603 | } 604 | 605 | // Highlight specific lines if they contain the specified word imp:1|test,3-5|test 606 | let isAlternativeHighlightedByLineSpecificWord = false; 607 | let isAlternativeHighlightedByLineSpecificWordColor = ''; 608 | const altLineSpecificWords = parameters.alternativeLinesToHighlight.lineSpecificWords; 609 | if (altLineSpecificWords.length > 0) { 610 | altLineSpecificWords.forEach(lsWord => { 611 | if (lsWord.lineNumber === lineNumber && lsWord.words.some(word => caseInsensitiveLineText.includes(word))) { 612 | isAlternativeHighlightedByLineSpecificWord = true; 613 | isAlternativeHighlightedByLineSpecificWordColor = lsWord.colorName; 614 | } 615 | }); 616 | } 617 | 618 | // Determine final highlight status and color 619 | if (isHighlightedByLineNumber || isHighlightedByWord || isHighlightedByLineSpecificWord) { 620 | result.isHighlighted = true; 621 | } else if (altHLMatch.length > 0) { 622 | result.isHighlighted = true; 623 | result.color = altHLMatch[0].colorName; // Assuming `colorName` is a property in the `lines` object 624 | } else if (isAlternativeHighlightedByWord) { 625 | result.isHighlighted = true; 626 | result.color = isAlternativeHighlightedByWordColor; 627 | } else if (isAlternativeHighlightedByLineSpecificWord) { 628 | result.isHighlighted = true; 629 | result.color = isAlternativeHighlightedByLineSpecificWordColor; 630 | } 631 | 632 | return result; 633 | }// isLineHighlighted 634 | 635 | async function highlightLines(preCodeElm: HTMLElement, parameters: Parameters, settings: ThemeSettings, indentationLevels: IndentationInfo[] | null, sourcePath: string, plugin: CodeBlockCustomizerPlugin) { 636 | if (!preCodeElm) 637 | return; 638 | 639 | const { htmlLines, textLines } = extractLinesFromHTML(preCodeElm); 640 | const codeblockLen = htmlLines.length - 1; 641 | const useSemiFold = codeblockLen >= settings.semiFold.visibleLines + fadeOutLineCount; 642 | 643 | let fadeOutLineIndex = 0; 644 | 645 | const totalLines = htmlLines.length - 1; 646 | const promptLines = computePromptLines(parameters, totalLines, plugin.settings); 647 | 648 | const { context, initialEnv } = createPromptContext(parameters, plugin.settings); 649 | let promptEnv = { ...initialEnv }; 650 | let cache: PromptCache = { key: "", node: null }; 651 | 652 | const frag = document.createDocumentFragment(); 653 | 654 | htmlLines.forEach((htmlLine, index) => { 655 | if (index === htmlLines.length - 1) 656 | return; 657 | 658 | const lineNumber = index + 1; 659 | const caseInsensitiveLineText = htmlLine.toLowerCase(); 660 | const hideCommandOutput = useSemiFold && lineNumber > settings.semiFold.visibleLines; 661 | 662 | const { lineWrapper, updatedFadeOutLineIndex } = getLineClass(lineNumber, caseInsensitiveLineText, parameters, settings, useSemiFold, fadeOutLineIndex); 663 | fadeOutLineIndex = updatedFadeOutLineIndex; 664 | 665 | const lineNumberEl = createLineNumberElement(lineNumber + parameters.lineNumberOffset, parameters.showNumbers); 666 | lineWrapper.appendChild(lineNumberEl); 667 | const textLine = textLines[index]; 668 | const isPromptLine = promptLines.has(lineNumber + parameters.lineNumberOffset); 669 | if (isPromptLine) { 670 | lineWrapper.classList.add("has-prompt"); 671 | const snapshot = { ...promptEnv }; 672 | const { /*promptData,*/ newEnv, newCache, node } = renderPromptLine(textLine, snapshot, cache, context); 673 | //const promptNode = addClassesToPrompt(promptData, context.isCustom ? context.promptDef.name : context.promptType, context.promptDef, plugin.settings, snapshot.user === "root"); 674 | //lineWrapper.appendChild(promptNode); 675 | lineWrapper.appendChild(node); 676 | 677 | promptEnv = newEnv; 678 | cache = newCache; 679 | } 680 | 681 | const indentedLine = addIndentLine(htmlLine, (indentationLevels && indentationLevels[lineNumber - 1]) ? indentationLevels[lineNumber - 1].insertCollapse : false); 682 | const lineTextEl = createLineTextElement(settings.codeblock.enableLinks ? parseInput(indentedLine, sourcePath, plugin) : indentedLine); 683 | textHighlight(parameters, lineNumber, lineTextEl); 684 | 685 | if (indentationLevels && indentationLevels[lineNumber - 1]) { 686 | const collapseIcon = lineTextEl.querySelector(".codeblock-customizer-collapse-icon"); 687 | if (collapseIcon) { 688 | setIcon(collapseIcon as HTMLElement, "chevron-down"); 689 | collapseIcon.addEventListener("click", handleClick); 690 | } 691 | } 692 | 693 | lineWrapper.appendChild(lineTextEl); 694 | lineWrapper.setAttribute("indentLevel", indentationLevels && indentationLevels[lineNumber - 1] ? indentationLevels[lineNumber - 1].indentationLevels.toString() : "-1"); 695 | frag.appendChild(lineWrapper); 696 | 697 | if (isPromptLine) { 698 | const outputLines = addCommandOutput(textLine, parameters, promptEnv, hideCommandOutput, lineNumber); 699 | for (const outputLine of outputLines) { 700 | frag.appendChild(outputLine); 701 | } 702 | } 703 | }); 704 | preCodeElm.appendChild(frag); 705 | }// highlightLines 706 | 707 | function addCommandOutput(lineText: string, parameters: Parameters, env: PromptEnvironment, hideCommandOutput: boolean, lineNumber: number) { 708 | const outputElements: HTMLElement[] = []; 709 | // pwd command 710 | if (/^\s*pwd\s*$/.test(lineText)) { 711 | outputElements.push(appendCommandOutputLine(getPWD(env), 'codeblock-customizer-prompt-cmd-output codeblock-customizer-workingdir', parameters, hideCommandOutput, lineText.toLowerCase(), lineNumber)); 712 | } 713 | 714 | // whoami command 715 | if (/^\s*whoami\s*$/.test(lineText)) 716 | outputElements.push(appendCommandOutputLine(env.user, 'codeblock-customizer-prompt-cmd-output codeblock-customizer-whoami', parameters, hideCommandOutput, lineText.toLowerCase(), lineNumber)); 717 | 718 | return outputElements; 719 | }// addCommandOutput 720 | 721 | function appendCommandOutputLine(text: string, cls: string, parameters: Parameters, hideCommandOutput: boolean, caseInsensitiveLineText: string, lineNumber: number) { 722 | const classes = ['has-prompt', 'codeblock-customizer-line', 'codeblock-customizer-cmdoutput-line']; 723 | 724 | if (hideCommandOutput) { 725 | classes.push('codeblock-customizer-fade-out-line-hide'); 726 | } 727 | 728 | const outputLine = createDiv({ cls: classes.join(' ') }); 729 | const outputText = createDiv({ cls: `${cls} codeblock-customizer-line-text`, text }); 730 | const result = isLineHighlighted(lineNumber, caseInsensitiveLineText, parameters); 731 | if (result.isHighlighted) { 732 | if (result.color) { 733 | outputLine.classList.add(`codeblock-customizer-line-highlighted-${result.color.replace(/\s+/g, '-').toLowerCase()}`); 734 | } else { 735 | outputLine.classList.add(`codeblock-customizer-line-highlighted`); 736 | } 737 | } 738 | 739 | const emptyLineNumber = createLineNumberElement(-1, parameters.showNumbers); 740 | outputLine.appendChild(emptyLineNumber); 741 | outputLine.appendChild(outputText); 742 | 743 | return outputLine; 744 | }// appendCommandOutputLine 745 | 746 | function getLineClass(lineNumber: number, caseInsensitiveLineText: string, parameters: Parameters, settings: ThemeSettings, useSemiFold: boolean, fadeOutLineIndex: number) { 747 | const lineWrapper = createDiv(); 748 | let updatedFadeOutLineIndex = fadeOutLineIndex; 749 | 750 | const result = isLineHighlighted(lineNumber, caseInsensitiveLineText, parameters); 751 | if (result.isHighlighted) { 752 | if (result.color) { 753 | lineWrapper.classList.add(`codeblock-customizer-line-highlighted-${result.color.replace(/\s+/g, '-').toLowerCase()}`); 754 | } else { 755 | lineWrapper.classList.add(`codeblock-customizer-line-highlighted`); 756 | } 757 | } else { 758 | lineWrapper.classList.add(`codeblock-customizer-line`); 759 | } 760 | 761 | if (useSemiFold && lineNumber > settings.semiFold.visibleLines && fadeOutLineIndex < fadeOutLineCount) { 762 | lineWrapper.classList.add(`codeblock-customizer-fade-out-line${fadeOutLineIndex}`); 763 | updatedFadeOutLineIndex++; 764 | if (fadeOutLineIndex === fadeOutLineCount - 1) { 765 | const uncollapseCodeButton = createUncollapseCodeButton(); 766 | uncollapseCodeButton.addEventListener("click", handleUncollapseClick); 767 | lineWrapper.appendChild(uncollapseCodeButton); 768 | } 769 | } 770 | 771 | if (useSemiFold && lineNumber > settings.semiFold.visibleLines + fadeOutLineCount) { 772 | lineWrapper.classList.add(`codeblock-customizer-fade-out-line-hide`); 773 | } 774 | 775 | return { lineWrapper, updatedFadeOutLineIndex }; 776 | }// getLineClass 777 | 778 | interface RangeToHighlight { 779 | nodesToHighlight: Node[]; 780 | startNode: Node; 781 | startOffset: number; 782 | endNode: Node; 783 | endOffset: number; 784 | } 785 | 786 | function textHighlight(parameters: Parameters, lineNumber: number, lineTextEl: HTMLDivElement) { 787 | const caseInsensitiveLineText = (lineTextEl.textContent ?? '').toLowerCase(); 788 | 789 | const wordHighlight = (words: string[], name = '') => { 790 | const caseInsensitiveWords = words.map(word => word.toLowerCase()); 791 | for (const word of caseInsensitiveWords) { 792 | highlightWords(lineTextEl, word, name); 793 | } 794 | }; 795 | 796 | const highlightBetween = (from: string, to: string, name = '') => { 797 | const caseInsensitiveFrom = from.toLowerCase(); 798 | const caseInsensitiveTo = to.toLowerCase(); 799 | 800 | const walkAndHighlight = (node: Node, searchTextFrom: string | null, searchTextTo: string | null) => { 801 | const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null); 802 | let firstNonWhitespaceNode: Node | null = null; 803 | let firstNonWhitespaceOffset = 0; 804 | let lastNode: Node | null = null; 805 | let lastNodeOffset = 0; 806 | const nodesToHighlight: Node[] = []; 807 | let searchTextToFound = false; 808 | 809 | while (walker.nextNode()) { 810 | const currentNode = walker.currentNode; 811 | const textContent = currentNode.textContent?.toLowerCase() || ''; 812 | 813 | if (!firstNonWhitespaceNode && textContent.trim().length > 0) { 814 | if (searchTextFrom) { 815 | if (textContent.includes(searchTextFrom)) { 816 | firstNonWhitespaceNode = currentNode; 817 | firstNonWhitespaceOffset = textContent.indexOf(searchTextFrom); 818 | } 819 | } else { 820 | firstNonWhitespaceNode = currentNode; 821 | firstNonWhitespaceOffset = textContent.search(/\S/); 822 | } 823 | } 824 | 825 | if (firstNonWhitespaceNode) { 826 | nodesToHighlight.push(currentNode); 827 | if (searchTextTo && textContent.includes(searchTextTo)) { 828 | const tempOffset = textContent.indexOf(searchTextTo) + searchTextTo.length; 829 | if (tempOffset > firstNonWhitespaceOffset) { 830 | lastNode = currentNode; 831 | lastNodeOffset = tempOffset; 832 | searchTextToFound = true; 833 | break; 834 | } else { 835 | let position = tempOffset; 836 | while ((position = textContent.indexOf(searchTextTo, position + 1)) !== -1) { 837 | if (position > firstNonWhitespaceOffset) { 838 | lastNode = currentNode; 839 | lastNodeOffset = position + searchTextTo.length; 840 | searchTextToFound = true; 841 | break; 842 | } 843 | } 844 | if (searchTextToFound) 845 | break; 846 | } 847 | } 848 | } 849 | } 850 | 851 | if (nodesToHighlight.length > 0 && firstNonWhitespaceNode && (searchTextFrom || searchTextToFound || (!searchTextFrom && !searchTextTo))) { 852 | const startNode = firstNonWhitespaceNode; 853 | const endNode = lastNode || nodesToHighlight[nodesToHighlight.length - 1]; 854 | const startOffset = firstNonWhitespaceOffset; 855 | const endOffset = lastNodeOffset || endNode.textContent?.length || 0; 856 | 857 | const rangeToHighlight: RangeToHighlight = { 858 | nodesToHighlight, 859 | startNode, 860 | startOffset, 861 | endNode, 862 | endOffset, 863 | }; 864 | 865 | highlightNodesRange(rangeToHighlight, name); 866 | } 867 | }; 868 | 869 | const highlightEntireText = (node: Node) => { 870 | walkAndHighlight(node, null, null); 871 | }; 872 | 873 | const highlightFromStart = (node: Node, searchTextFrom: string) => { 874 | walkAndHighlight(node, searchTextFrom, null); 875 | }; 876 | 877 | const highlightUntilEnd = (node: Node, searchTextTo: string) => { 878 | walkAndHighlight(node, null, searchTextTo); 879 | }; 880 | 881 | /*const highlightFromTo = (node: Node, searchTextFrom: string, searchTextTo: string) => { 882 | walkAndHighlight(node, searchTextFrom, searchTextTo); 883 | };*/ 884 | 885 | if (!caseInsensitiveFrom && !caseInsensitiveTo) { 886 | highlightEntireText(lineTextEl); 887 | } else if (caseInsensitiveFrom && !caseInsensitiveTo) { 888 | highlightFromStart(lineTextEl, caseInsensitiveFrom.toLowerCase()); 889 | } else if (!caseInsensitiveFrom && caseInsensitiveTo) { 890 | highlightUntilEnd(lineTextEl, caseInsensitiveTo.toLowerCase()); 891 | } else if (caseInsensitiveFrom && caseInsensitiveTo) { 892 | //highlightFromTo(lineTextEl, caseInsensitiveFrom.toLowerCase(), caseInsensitiveTo.toLowerCase()); 893 | highlightFromTo(lineTextEl, from, to, name); 894 | } 895 | }; 896 | 897 | const highlightNodesRange = (range: RangeToHighlight, name: string) => { 898 | const { nodesToHighlight, startNode, startOffset, endNode, endOffset } = range; 899 | let currentStartOffset = startOffset; // Change this line 900 | 901 | for (const currentNode of nodesToHighlight) { 902 | if (currentNode.nodeType === Node.TEXT_NODE) { 903 | const span = createSpan(); 904 | span.className = name ? `codeblock-customizer-highlighted-text-${name}` : 'codeblock-customizer-highlighted-text'; 905 | 906 | let textToHighlight = ''; 907 | if (currentNode === startNode && currentNode === endNode) { 908 | textToHighlight = currentNode.textContent?.substring(currentStartOffset, endOffset) || ''; 909 | } else if (currentNode === startNode) { 910 | textToHighlight = currentNode.textContent?.substring(currentStartOffset) || ''; 911 | } else if (currentNode === endNode) { 912 | textToHighlight = currentNode.textContent?.substring(0, endOffset) || ''; 913 | } else { 914 | textToHighlight = currentNode.textContent || ''; 915 | } 916 | 917 | span.appendChild(document.createTextNode(textToHighlight)); 918 | 919 | const beforeText = document.createTextNode(currentNode.textContent?.substring(0, currentStartOffset) || ''); 920 | const afterText = currentNode === endNode ? document.createTextNode(currentNode.textContent?.substring(endOffset) || '') : document.createTextNode(''); 921 | 922 | const parentNode = currentNode.parentNode; 923 | if (parentNode) { 924 | parentNode.replaceChild(afterText, currentNode); 925 | parentNode.insertBefore(span, afterText); 926 | parentNode.insertBefore(beforeText, span); 927 | } 928 | 929 | currentStartOffset = 0; // Reset startOffset after the first node 930 | } 931 | } 932 | }; 933 | 934 | // highlight text in every line if linetext contains the specified word hlt:test 935 | const words = parameters.defaultTextToHighlight.words; 936 | if (words.length > 0) { 937 | wordHighlight(words); 938 | } 939 | 940 | // highlight text in specific lines if linetext contains the specified word hlt:1|test,3-5|test 941 | const lineSpecificWords = parameters.defaultTextToHighlight.lineSpecificWords; 942 | const lineSpecificWord = lineSpecificWords.find(item => item.lineNumber === lineNumber); 943 | if (lineSpecificWord) { 944 | wordHighlight(lineSpecificWord.words); 945 | } 946 | 947 | // highlight text with specific text between markers hlt:start:end 948 | const textBetween = parameters.defaultTextToHighlight.textBetween; 949 | for (const { from, to } of textBetween) { 950 | if (caseInsensitiveLineText.includes(from.toLowerCase()) && caseInsensitiveLineText.includes(to.toLowerCase())) { 951 | highlightBetween(from, to); 952 | } 953 | } 954 | 955 | // highlight text within specific lines with text between markers hl:5|start:end, hlt:5-7|start:end 956 | const lineSpecificTextBetween = parameters.defaultTextToHighlight.lineSpecificTextBetween; 957 | const specificTextBetween = lineSpecificTextBetween.find(item => item.lineNumber === lineNumber); 958 | if (specificTextBetween) { 959 | if (caseInsensitiveLineText.includes(specificTextBetween.from.toLowerCase()) && caseInsensitiveLineText.includes(specificTextBetween.to.toLowerCase())) { 960 | highlightBetween(specificTextBetween.from, specificTextBetween.to); 961 | } 962 | } 963 | 964 | // highlight all words in specified line hlt:1,3-5 965 | if (parameters.defaultTextToHighlight.allWordsInLine.includes(lineNumber)) { 966 | highlightBetween('', ''); 967 | } 968 | 969 | // highlight text in every line if linetext contains the specified word impt:test 970 | const altWords = parameters.alternativeTextToHighlight.words; 971 | for (const entry of altWords) { 972 | const { colorName, words } = entry; 973 | if (words.length > 0) { 974 | wordHighlight(words, colorName); 975 | } 976 | } 977 | 978 | // highlight text in specific lines if linetext contains the specified word impt:1|test,3-5|test 979 | const altLineSpecificWords = parameters.alternativeTextToHighlight.lineSpecificWords; 980 | const altLineSpecificWord = altLineSpecificWords.find(item => item.lineNumber === lineNumber); 981 | if (altLineSpecificWord) { 982 | const { colorName, words } = altLineSpecificWord; 983 | wordHighlight(words, colorName); 984 | } 985 | 986 | // highlight text with specific text between markers impt:start:end 987 | const altTextBetween = parameters.alternativeTextToHighlight.textBetween; 988 | altTextBetween.forEach(({ from, to, colorName }) => { 989 | highlightBetween(from, to, colorName); 990 | }); 991 | 992 | // highlight text within specific lines with text between markers impt:5|start:end, imp:5-7|start:end 993 | const altLineSpecificTextBetween = parameters.alternativeTextToHighlight.lineSpecificTextBetween; 994 | const altSpecificTextBetween = altLineSpecificTextBetween.find(item => item.lineNumber === lineNumber); 995 | if (altSpecificTextBetween) { 996 | altLineSpecificTextBetween.forEach(({ lineNumber: altLineNumber, from, to, colorName }) => { 997 | if (lineNumber === altLineNumber) { 998 | highlightBetween(from, to, colorName); 999 | } 1000 | }); 1001 | } 1002 | 1003 | // highlight all words in specified line impt:1,3-5 1004 | const altAllWordsInLine = parameters.alternativeTextToHighlight.allWordsInLine; 1005 | const altAllWordsInLineMatch = altAllWordsInLine.find(item => item.allWordsInLine.includes(lineNumber)); 1006 | if (altAllWordsInLineMatch) { 1007 | highlightBetween('','', altAllWordsInLineMatch.colorName); 1008 | } 1009 | }// textHighlight 1010 | 1011 | function highlightFromTo(node: Node, from: string, to: string, alternativeName?: string): void { 1012 | const className = alternativeName 1013 | ? `codeblock-customizer-highlighted-text-${alternativeName}` 1014 | : `codeblock-customizer-highlighted-text`; 1015 | 1016 | const createSpan = (text: string): HTMLSpanElement => { 1017 | const span = document.createElement('span'); 1018 | span.className = className; 1019 | span.appendChild(document.createTextNode(text)); 1020 | return span; 1021 | }; 1022 | 1023 | const collectTextNodes = (node: Node, textNodes: Text[]): void => { 1024 | if (node.nodeType === Node.TEXT_NODE) { 1025 | textNodes.push(node as Text); 1026 | } else { 1027 | node.childNodes.forEach(child => collectTextNodes(child, textNodes)); 1028 | } 1029 | }; 1030 | 1031 | const highlightRanges = (textNodes: Text[], ranges: { start: number, end: number }[]): void => { 1032 | let currentIndex = 0; 1033 | let currentRangeIndex = 0; 1034 | let currentRange = ranges[currentRangeIndex]; 1035 | 1036 | textNodes.forEach(textNode => { 1037 | if (!currentRange) return; 1038 | const textContent = textNode.textContent || ''; 1039 | const fragment = document.createDocumentFragment(); 1040 | let lastIndex = 0; 1041 | 1042 | while (currentRange && lastIndex < textContent.length) { 1043 | const rangeStart = currentRange.start - currentIndex; 1044 | const rangeEnd = currentRange.end - currentIndex; 1045 | 1046 | if (rangeStart >= 0 && rangeStart < textContent.length) { 1047 | // Text before the range 1048 | if (rangeStart > lastIndex) { 1049 | fragment.appendChild(document.createTextNode(textContent.substring(lastIndex, rangeStart))); 1050 | } 1051 | 1052 | // Text within the range 1053 | if (rangeEnd <= textContent.length) { 1054 | fragment.appendChild(createSpan(textContent.substring(rangeStart, rangeEnd))); 1055 | lastIndex = rangeEnd; 1056 | currentRangeIndex++; 1057 | currentRange = ranges[currentRangeIndex]; 1058 | } else { 1059 | fragment.appendChild(createSpan(textContent.substring(rangeStart))); 1060 | lastIndex = textContent.length; 1061 | currentRange.start += textContent.length - rangeStart; 1062 | } 1063 | } else { 1064 | break; 1065 | } 1066 | } 1067 | 1068 | // Append remaining text 1069 | if (lastIndex < textContent.length) { 1070 | fragment.appendChild(document.createTextNode(textContent.substring(lastIndex))); 1071 | } 1072 | 1073 | const parentNode = textNode.parentNode; 1074 | if (parentNode) { 1075 | parentNode.replaceChild(fragment, textNode); 1076 | } 1077 | 1078 | currentIndex += textContent.length; 1079 | }); 1080 | }; 1081 | 1082 | const findRanges = (text: string, from: string, to: string): { start: number, end: number }[] => { 1083 | const ranges = []; 1084 | let startIndex = text.toLowerCase().indexOf(from.toLowerCase()); 1085 | 1086 | while (startIndex !== -1) { 1087 | const endIndex = text.toLowerCase().indexOf(to.toLowerCase(), startIndex + from.length); 1088 | if (endIndex === -1) break; 1089 | 1090 | ranges.push({ start: startIndex, end: endIndex + to.length }); 1091 | startIndex = text.toLowerCase().indexOf(from.toLowerCase(), endIndex + to.length); 1092 | } 1093 | 1094 | return ranges; 1095 | }; 1096 | 1097 | const textNodes: Text[] = []; 1098 | collectTextNodes(node, textNodes); 1099 | 1100 | const concatenatedText = textNodes.map(node => node.textContent).join(''); 1101 | const ranges = findRanges(concatenatedText, from, to); 1102 | 1103 | highlightRanges(textNodes, ranges); 1104 | }// highlightFromTo 1105 | 1106 | function highlightWords(node: Node, word: string, alternativeName?: string): void { 1107 | if (!word) return; 1108 | 1109 | const lowerCaseWord = word.toLowerCase(); 1110 | const className = alternativeName 1111 | ? `codeblock-customizer-highlighted-text-${alternativeName}` 1112 | : `codeblock-customizer-highlighted-text`; 1113 | 1114 | const createSpan = (text: string): HTMLSpanElement => { 1115 | const span = document.createElement('span'); 1116 | span.className = className; 1117 | span.appendChild(document.createTextNode(text)); 1118 | return span; 1119 | }; 1120 | 1121 | const processTextNode = (textNode: Text): void => { 1122 | const textContent = textNode.textContent || ''; 1123 | const occurrences = findAllOccurrences(textContent.toLowerCase(), lowerCaseWord); 1124 | 1125 | if (occurrences.length === 0) return; 1126 | 1127 | const parentNode = textNode.parentNode; 1128 | if (!parentNode) return; 1129 | 1130 | const fragment = document.createDocumentFragment(); 1131 | let lastIndex = 0; 1132 | 1133 | occurrences.forEach(index => { 1134 | const beforeText = textContent.substring(lastIndex, index); 1135 | const matchText = textContent.substring(index, index + word.length); 1136 | 1137 | if (beforeText) { 1138 | fragment.appendChild(document.createTextNode(beforeText)); 1139 | } 1140 | fragment.appendChild(createSpan(matchText)); 1141 | lastIndex = index + word.length; 1142 | }); 1143 | 1144 | const remainingText = textContent.substring(lastIndex); 1145 | if (remainingText) { 1146 | fragment.appendChild(document.createTextNode(remainingText)); 1147 | } 1148 | 1149 | parentNode.replaceChild(fragment, textNode); 1150 | }; 1151 | 1152 | const walkTree = (node: Node): void => { 1153 | const textNodes: Text[] = []; 1154 | const collectTextNodes = (node: Node) => { 1155 | if (node.nodeType === Node.TEXT_NODE) { 1156 | textNodes.push(node as Text); 1157 | } else if (node.nodeType === Node.ELEMENT_NODE) { 1158 | Array.from(node.childNodes).forEach(collectTextNodes); 1159 | } 1160 | }; 1161 | 1162 | collectTextNodes(node); 1163 | textNodes.forEach(processTextNode); 1164 | }; 1165 | 1166 | walkTree(node); 1167 | }// highlightWords 1168 | 1169 | function parseInput(input: string, sourcePath: string, plugin: CodeBlockCustomizerPlugin): string { 1170 | if (input === "") 1171 | return input; 1172 | 1173 | // #98 1174 | const placeholder = '\u200B'; // Zero-width space 1175 | const inputWithPlaceholders = input.replace(/(^\s{1,3})/gm, (match) => placeholder.repeat(match.length)); 1176 | 1177 | const parser = new DOMParser(); 1178 | const doc = parser.parseFromString(inputWithPlaceholders, 'text/html'); 1179 | const elementsWithClass = Array.from(doc.getElementsByClassName('comment')); 1180 | const regex = /(?:\[\[([^[\]]+?)(?:\|([^\]]+?))?]]|\[([^\]]+)\]\(([^)]+)\)|(https?:\/\/[^\s]+))/g; 1181 | 1182 | elementsWithClass.forEach((element: Element) => { 1183 | const textContent = element.textContent || ''; 1184 | let lastIndex = 0; 1185 | let match; 1186 | 1187 | const fragment = document.createDocumentFragment(); 1188 | 1189 | while ((match = regex.exec(textContent)) !== null) { 1190 | const textBeforeMatch = textContent.slice(lastIndex, match.index); 1191 | fragment.appendChild(document.createTextNode(textBeforeMatch)); 1192 | 1193 | const span = createSpan({cls: "codeblock-customizer-link"}); 1194 | MarkdownRenderer.render(plugin.app, match[0], span, sourcePath, plugin); 1195 | fragment.appendChild(span); 1196 | 1197 | lastIndex = match.index + match[0].length; 1198 | } 1199 | 1200 | const textAfterLastMatch = textContent.slice(lastIndex); 1201 | fragment.appendChild(document.createTextNode(textAfterLastMatch)); 1202 | 1203 | element.textContent = ''; 1204 | element.appendChild(fragment); 1205 | }); 1206 | 1207 | const output = new XMLSerializer().serializeToString(doc); 1208 | return output.replace(new RegExp(placeholder, 'g'), ' '); 1209 | }// parseInput 1210 | 1211 | function handleClick(event: Event) { 1212 | const collapseIcon = event.currentTarget as HTMLElement; 1213 | if (!collapseIcon) 1214 | return; 1215 | 1216 | const codeElement = getCodeElementFromCollapseIcon(collapseIcon); 1217 | if (!codeElement) 1218 | return; 1219 | 1220 | const collapseIconParent = getParentWithClassStartingWith(collapseIcon, "codeblock-customizer-line"); 1221 | if (!collapseIconParent) 1222 | return; 1223 | collapseIconParent.classList.toggle("codeblock-customizer-lines-below-collapsed"); 1224 | 1225 | const clickedIndentLevel = parseInt(collapseIconParent.getAttribute('indentlevel') || ""); 1226 | const codeLines = Array.from(codeElement.querySelectorAll('[class^="codeblock-customizer-line"]')); 1227 | 1228 | let lessEqualIndent = false; 1229 | let startPosReached = false; 1230 | let startPosLineId = -1; 1231 | const lines: { element: HTMLElement; lineCount: number }[] = []; 1232 | let lineCount = 0; 1233 | for (const line of codeLines) { 1234 | if (line.getAttribute('indentlevel') === null) 1235 | continue; 1236 | 1237 | if (collapseIconParent === line) { 1238 | startPosReached = true; 1239 | startPosLineId = lineCount; 1240 | } 1241 | 1242 | const lineIndentLevel = parseInt(line.getAttribute('indentlevel') || ""); 1243 | if (lineIndentLevel > clickedIndentLevel && startPosReached) { 1244 | lines.push({ element: line as HTMLElement, lineCount }); 1245 | lessEqualIndent = true; 1246 | } else if (lessEqualIndent && lineIndentLevel <= clickedIndentLevel) { 1247 | break; 1248 | } 1249 | lineCount++; 1250 | } 1251 | 1252 | if (collapseIconParent.classList.contains("codeblock-customizer-lines-below-collapsed")) { 1253 | setIcon(collapseIcon, "chevron-right"); 1254 | for (const line of lines) { 1255 | const lineTextEl = collapseIconParent.querySelector('.codeblock-customizer-line-text'); 1256 | if (lineTextEl) { 1257 | const foldPlaceholder = createSpan({text: "…", cls: 'codeblock-customizer-foldPlaceholder'}); 1258 | const existingFoldPlaceholder = lineTextEl.querySelector('.codeblock-customizer-foldPlaceholder'); 1259 | if (!existingFoldPlaceholder) { 1260 | lineTextEl.appendChild(foldPlaceholder); 1261 | } 1262 | } 1263 | line.element.classList.add('codeblock-customizer-line-hidden'); 1264 | if (line.element.getAttribute('collapsedBy') === null) 1265 | line.element.setAttribute('collapsedBy', startPosLineId.toString()); 1266 | } 1267 | } else { 1268 | setIcon(collapseIcon, "chevron-down"); 1269 | for (const line of lines) { 1270 | if (parseInt(line.element.getAttribute("collapsedBy") || "") === startPosLineId) { 1271 | line.element.classList.remove('codeblock-customizer-line-hidden'); 1272 | line.element.removeAttribute('collapsedBy'); 1273 | const lineTextEl = collapseIconParent.querySelector('.codeblock-customizer-line-text'); 1274 | if (lineTextEl) { 1275 | const existingFoldPlaceholder = lineTextEl.querySelector('.codeblock-customizer-foldPlaceholder'); 1276 | if (existingFoldPlaceholder) { 1277 | existingFoldPlaceholder.remove(); 1278 | } 1279 | } 1280 | } 1281 | } 1282 | } 1283 | }// handleClick 1284 | 1285 | function getCodeElementFromCollapseIcon(collapseIcon: HTMLElement): HTMLElement | null { 1286 | let parentElement = collapseIcon.parentElement; 1287 | while (parentElement) { 1288 | if (parentElement.classList.contains('codeblock-customizer-pre')) { 1289 | const codeElements = parentElement.querySelector('code'); 1290 | if (codeElements) 1291 | return codeElements; 1292 | } 1293 | parentElement = parentElement.parentElement; 1294 | } 1295 | return null; 1296 | }// getCodeElementFromCollapseIcon 1297 | 1298 | function getParentWithClassStartingWith(element: HTMLElement, classNamePrefix: string) { 1299 | let parent = element.parentElement; 1300 | while (parent) { 1301 | const classList = parent.classList; 1302 | if (classList && Array.from(classList).some((className) => className.startsWith(classNamePrefix))) { 1303 | const indentLevel = parent.getAttribute('indentlevel'); 1304 | if (indentLevel !== null) { 1305 | return parent; 1306 | } 1307 | } 1308 | parent = parent.parentElement; 1309 | } 1310 | return null; 1311 | }// getParentWithClassStartingWith 1312 | 1313 | function handleUncollapseClick(event: Event) { 1314 | const button = event.target as HTMLElement; 1315 | 1316 | const codeElement = button.parentElement?.parentElement; 1317 | if (!codeElement) 1318 | return; 1319 | 1320 | const pre = codeElement?.parentElement; 1321 | if (!pre) 1322 | return; 1323 | 1324 | let header: HTMLElement; 1325 | if (pre.classList.contains("displayedInGroup")) { 1326 | // grouped code blocks 1327 | const group = pre.getAttribute("groupname"); 1328 | header = document.querySelector(`.markdown-rendered .codeblock-customizer-pre-parent .codeblock-customizer-header-group-container[group="${group}"]`) as HTMLElement; 1329 | } else { 1330 | // ungrouped code blocks 1331 | header = button.parentElement?.parentElement?.previousSibling?.previousSibling as HTMLElement; 1332 | } 1333 | 1334 | if (header) { 1335 | const collapseIcon = header.querySelector(".codeblock-customizer-header-collapse") as HTMLElement; 1336 | if (collapseIcon && pre) { 1337 | toggleFold(pre, collapseIcon, `codeblock-customizer-codeblock-semi-collapsed`); 1338 | } 1339 | } 1340 | }// handleUncollapseClick 1341 | 1342 | export function toggleFold(pre: HTMLElement, collapseIcon: HTMLElement, toggleClass: string) { 1343 | if (pre?.classList.contains(toggleClass)) { 1344 | setIcon(collapseIcon, "chevrons-up-down"); 1345 | } else { 1346 | setIcon(collapseIcon, "chevrons-down-up"); 1347 | } 1348 | pre?.classList.toggle(toggleClass); 1349 | }// toggleFold 1350 | 1351 | export function convertHTMLCollectionToArray(elements: HTMLCollection, excludeCmdOutput = false) { 1352 | const result: Element[] = []; 1353 | for (let i = 0; i < elements.length; i++ ){ 1354 | const children = Array.from(elements[i].children); 1355 | if (excludeCmdOutput) { 1356 | result.push(...children.filter(child => !child.classList.contains('codeblock-customizer-cmdoutput-line'))); 1357 | } else { 1358 | result.push(...children); 1359 | } 1360 | } 1361 | return result; 1362 | }// convertHTMLCollectionToArray 1363 | 1364 | async function PDFExport(codeBlockElement: HTMLElement[], plugin: CodeBlockCustomizerPlugin, codeBlockFirstLines: string[], sourcePath: string) { 1365 | for (const [key, codeblockPreElement] of Array.from(codeBlockElement).entries()) { 1366 | const codeblockParameters = codeBlockFirstLines[key]; 1367 | const parameters = getAllParameters(codeblockParameters, plugin.settings); 1368 | 1369 | const codeblockCodeElement: HTMLPreElement | null = codeblockPreElement.querySelector("pre > code"); 1370 | if (!codeblockCodeElement) 1371 | return; 1372 | 1373 | if (Array.from(codeblockCodeElement.classList).some(className => /^language-\S+/.test(className))) 1374 | while(!codeblockCodeElement.classList.contains("is-loaded")) 1375 | await sleep(2); 1376 | 1377 | if (codeblockCodeElement.querySelector("code [class*='codeblock-customizer-line']")) 1378 | continue; 1379 | 1380 | if (parameters.exclude) 1381 | continue; 1382 | 1383 | if (plugin.settings.SelectedTheme.settings.printing.uncollapseDuringPrint) 1384 | parameters.fold = false; 1385 | 1386 | const codeblockLanguageSpecificClass = getLanguageSpecificColorClass(parameters.language, plugin.settings.SelectedTheme.colors[getCurrentMode()].languageSpecificColors); 1387 | await addClasses(codeblockPreElement, parameters, plugin, codeblockCodeElement as HTMLElement, null, codeblockLanguageSpecificClass, sourcePath); 1388 | } 1389 | }// PDFExport 1390 | 1391 | // does not support if folding is disabled 1392 | export function foldAllReadingView(fold: boolean, settings: CodeblockCustomizerSettings) { 1393 | const preParents = document.querySelectorAll('.codeblock-customizer-pre-parent'); 1394 | preParents.forEach((preParent) => { 1395 | const preElement = preParent.querySelector('.codeblock-customizer-pre'); 1396 | 1397 | let lines: Element[] = []; 1398 | if (preElement){ 1399 | const codeElements = preElement?.getElementsByTagName("CODE"); 1400 | lines = convertHTMLCollectionToArray(codeElements, true); 1401 | } 1402 | 1403 | toggleFoldClasses(preElement as HTMLPreElement, lines.length, fold, settings.SelectedTheme.settings.semiFold.enableSemiFold, settings.SelectedTheme.settings.semiFold.visibleLines); 1404 | }); 1405 | }//foldAllreadingView 1406 | 1407 | export function toggleFoldClasses(preElement: HTMLPreElement, linesLength: number, fold: boolean, enableSemiFold: boolean, visibleLines: number) { 1408 | if (fold) { 1409 | if (enableSemiFold) { 1410 | if (linesLength >= visibleLines + fadeOutLineCount) { 1411 | preElement?.classList.add('codeblock-customizer-codeblock-semi-collapsed'); 1412 | } else 1413 | preElement?.classList.add('codeblock-customizer-codeblock-collapsed'); 1414 | } 1415 | else 1416 | preElement?.classList.add('codeblock-customizer-codeblock-collapsed'); 1417 | } 1418 | else { 1419 | if (enableSemiFold) { 1420 | if (linesLength >= visibleLines + fadeOutLineCount) { 1421 | preElement?.classList.remove('codeblock-customizer-codeblock-semi-collapsed'); 1422 | } else 1423 | preElement?.classList.remove('codeblock-customizer-codeblock-collapsed'); 1424 | } else 1425 | preElement?.classList.remove('codeblock-customizer-codeblock-collapsed'); 1426 | } 1427 | }// toggleFoldClasses 1428 | 1429 | function getCodeBlocksFirstLines(array: string[]): string[] { 1430 | if (!array || !Array.isArray(array)) 1431 | return []; 1432 | 1433 | const codeBlocks: string[] = []; 1434 | let inCodeBlock = false; 1435 | let openingBackticks = 0; 1436 | 1437 | for (let i = 0; i < array.length; i++) { 1438 | let line = array[i] ?? ""; 1439 | line = removeCharFromStart(line.trim(), ">"); 1440 | 1441 | const backtickMatch = line.match(/^`+(?!.*`)/); 1442 | if (backtickMatch) { 1443 | if (!inCodeBlock) { 1444 | inCodeBlock = true; 1445 | openingBackticks = backtickMatch[0].length; 1446 | codeBlocks.push(line); 1447 | } else { 1448 | if (backtickMatch[0].length === openingBackticks) { 1449 | inCodeBlock = false; 1450 | openingBackticks = 0; 1451 | } 1452 | } 1453 | } 1454 | } 1455 | 1456 | // Handle the case when the last block is requested 1457 | if (codeBlocks.length > 0) { 1458 | //const firstLineOfBlock = currentBlock[0]; 1459 | return codeBlocks; 1460 | } 1461 | 1462 | return []; 1463 | }// getCodeBlocksFirstLine 1464 | 1465 | function getCallouts(array: string[]): string[] { 1466 | if (!array) 1467 | return []; 1468 | 1469 | const arrowBlocks: string[] = []; 1470 | 1471 | for (let i = 0; i < array.length; i++) { 1472 | const line = array[i].trim(); 1473 | if (line.startsWith(">")) { 1474 | arrowBlocks.push(line); 1475 | } 1476 | } 1477 | 1478 | const arrowBlocksResult: string[] = getCodeBlocksFirstLines(arrowBlocks); 1479 | 1480 | if (arrowBlocksResult.length > 0) 1481 | return arrowBlocksResult; 1482 | else 1483 | return []; 1484 | }// getCallouts 1485 | -------------------------------------------------------------------------------- /src/Settings.ts: -------------------------------------------------------------------------------- 1 | import { PromptDefinition } from "./Utils"; 2 | 3 | export interface Colors { 4 | codeblock: { 5 | activeLineColor: string; 6 | backgroundColor: string; 7 | highlightColor: string; 8 | alternateHighlightColors: Record; 9 | languageBorderColors: Record; 10 | textColor: string; 11 | bracketHighlightColorMatch: string; 12 | bracketHighlightColorNoMatch: string; 13 | bracketHighlightBackgroundColorMatch: string; 14 | bracketHighlightBackgroundColorNoMatch: string; 15 | selectionMatchHighlightColor: string; 16 | }, 17 | header: { 18 | backgroundColor: string; 19 | textColor: string; 20 | lineColor: string; 21 | codeBlockLangTextColor: string; 22 | codeBlockLangBackgroundColor: string; 23 | }, 24 | gutter: { 25 | textColor: string; 26 | backgroundColor: string; 27 | activeLineNrColor: string; 28 | }, 29 | inlineCode: { 30 | backgroundColor: string; 31 | textColor: string; 32 | }, 33 | prompts: { 34 | promptColors?: Record>; 35 | rootPromptColors?: Record>; 36 | editedPromptColors: Record>; 37 | editedRootPromptColors: Record>; 38 | }, 39 | groupedCodeBlocks: { 40 | activeTabBackgroundColor: string; 41 | hoverTabBackgroundColor: string; 42 | hoverTabTextColor: string; 43 | }, 44 | editorActiveLineColor: string; 45 | languageSpecificColors: Record>; 46 | } 47 | 48 | export interface ThemeColors { 49 | dark: Colors; 50 | light: Colors; 51 | } 52 | 53 | export interface ThemeSettings { 54 | codeblock: { 55 | enableLineNumbers: boolean; 56 | enableActiveLineHighlight: boolean; 57 | codeBlockBorderStylingPosition: string; 58 | showIndentationLines: boolean; 59 | enableLinks: boolean; 60 | enableLinkUpdate: boolean; 61 | enableBracketHighlight: boolean; 62 | highlightNonMatchingBrackets: boolean; 63 | inverseFold: boolean; 64 | enableSelectionMatching: boolean; 65 | unwrapcode: boolean; 66 | buttons: { 67 | alwaysShowButtons: boolean; 68 | alwaysShowCopyCodeButton: boolean; 69 | enableSelectCodeButton: boolean; 70 | enableWrapCodeButton: boolean; 71 | enableDeleteCodeButton: boolean; 72 | }, 73 | }, 74 | textHighlight: { 75 | lineSeparator: string; 76 | textSeparator: string; 77 | }, 78 | semiFold: { 79 | enableSemiFold: boolean; 80 | visibleLines: number; 81 | showAdditionalUncollapseButon: boolean; 82 | }, 83 | header: { 84 | boldText: boolean; 85 | italicText: boolean; 86 | collapseIconPosition: string; 87 | collapsedCodeText: string; 88 | codeblockLangBoldText: boolean; 89 | codeblockLangItalicText: boolean; 90 | alwaysDisplayCodeblockLang: boolean; 91 | alwaysDisplayCodeblockIcon: boolean; 92 | displayCodeBlockLanguage: boolean; 93 | displayCodeBlockIcon: boolean; 94 | disableFoldUnlessSpecified: boolean; 95 | }, 96 | gutter: { 97 | highlightActiveLineNr: boolean; 98 | enableHighlight: boolean; 99 | }, 100 | inlineCode: { 101 | enableInlineCodeStyling: boolean; 102 | }, 103 | printing: { 104 | enablePrintToPDFStyling: boolean; 105 | forceCurrentColorUse: boolean; 106 | uncollapseDuringPrint: boolean; 107 | }, 108 | common: { 109 | enableInSourceMode: boolean; 110 | }, 111 | prompts: { 112 | editedDefaults: Record>; 113 | customPrompts: Record; 114 | }, 115 | enableEditorActiveLineHighlight: boolean; 116 | } 117 | 118 | export interface Theme { 119 | baseTheme?: string; 120 | settings: ThemeSettings; 121 | colors: ThemeColors; 122 | } 123 | 124 | export interface CodeblockCustomizerSettings { 125 | Themes: Record; 126 | ExcludeLangs: string; 127 | ThemeName: string; 128 | SelectedTheme: Theme; 129 | newThemeName: string; 130 | newPromptName: string; 131 | alternateHighlightColorName: string; 132 | languageBorderColorName: string; 133 | foldAllCommand: boolean; 134 | settingsType: string; 135 | langSpecificSettingsType: string; 136 | languageSpecificLanguageName: string; 137 | } 138 | 139 | // dark 140 | export const D_ACTIVE_CODEBLOCK_LINE_COLOR = '#073642'; 141 | export const D_ACTIVE_LINE_COLOR = '#468eeb33'; 142 | export const D_BACKGROUND_COLOR = '#002B36'; 143 | export const D_HIGHLIGHT_COLOR = '#054b5c'; 144 | export const D_HEADER_COLOR = '#0a4554'; 145 | export const D_HEADER_TEXT_COLOR = '#DADADA'; 146 | export const D_HEADER_LINE_COLOR = '#46cced'; 147 | export const D_GUTTER_TEXT_COLOR = '#6c6c6c'; 148 | export const D_GUTTER_BACKGROUND_COLOR = '#073642'; 149 | export const D_LANG_COLOR = '#000000'; 150 | export const D_LANG_BACKGROUND_COLOR = '#008080'; 151 | export const D_GUTTER_ACTIVE_LINENR_COLOR = '#DADADA'; 152 | export const D_INLINE_CODE_BACKGROUND_COLOR = '#054b5c'; 153 | export const D_INLINE_CODE_TEXT_COLOR = '#DADADA'; 154 | 155 | // light 156 | export const L_ACTIVE_CODEBLOCK_LINE_COLOR = '#EDE8D6'; 157 | export const L_ACTIVE_LINE_COLOR = '#60460633'; 158 | export const L_BACKGROUND_COLOR = '#FCF6E4'; 159 | export const L_HIGHLIGHT_COLOR = '#E9DFBA'; 160 | export const L_HEADER_COLOR = '#D5CCB4'; 161 | export const L_HEADER_TEXT_COLOR = '#866704'; 162 | export const L_HEADER_LINE_COLOR = '#EDD489'; 163 | export const L_GUTTER_TEXT_COLOR = '#6c6c6c'; 164 | export const L_GUTTER_BACKGROUND_COLOR = '#EDE8D6'; 165 | export const L_LANG_COLOR = '#C25F30'; 166 | export const L_LANG_BACKGROUND_COLOR = '#B8B5AA'; 167 | export const L_GUTTER_ACTIVE_LINENR_COLOR = '#866704'; 168 | export const L_INLINE_CODE_BACKGROUND_COLOR = '#E9DFBA'; 169 | export const L_INLINE_CODE_TEXT_COLOR = '#866704'; 170 | 171 | const SELECTION_MATCH_COLOR = '#99ff7780'; 172 | 173 | const DarkPromptColors: Record> = { 174 | "bash": { 175 | "prompt-user": "#61afef", 176 | "prompt-host": "#e5c07b", 177 | "prompt-path": "#98c379", 178 | }, 179 | "bashalt": { 180 | "prompt-user": "#61afef", 181 | "prompt-host": "#d19a66", 182 | "prompt-path": "#56b6c2", 183 | "prompt-hash": "#ff5555", 184 | }, 185 | "kali": { 186 | "prompt-user": "#2679F2", 187 | "prompt-host": "#2679F2", 188 | "prompt-path": "#F3F3F4", 189 | "prompt-kali-symbol": "#2679F2", 190 | "prompt-dollar": "#2679F2", 191 | "prompt-dash": "#56AA9B", 192 | "prompt-bracket-open": "#56AA9B", 193 | "prompt-bracket-close": "#56AA9B", 194 | "prompt-square-open": "#56AA9B", 195 | "prompt-square-close": "#56AA9B", 196 | }, 197 | "zshgit": { 198 | "prompt-path": "#61afef", 199 | "prompt-branch": "#c678dd", 200 | "prompt-zsh-status-error": "#ff5555", 201 | "prompt-zsh-status-ok": "#50fa7b", 202 | "prompt-zsh-symbol": "#00ff00", 203 | "prompt-symbol": "#8be9fd", 204 | }, 205 | "zsh": { 206 | "prompt-user": "#56b6c2", 207 | "prompt-host": "#e06c75", 208 | "prompt-path": "#98c379", 209 | "prompt-percent": "#abb2bf", 210 | }, 211 | "fish": { 212 | "prompt-path": "#61afef", 213 | }, 214 | "ps": { 215 | "prompt-path": "#5b9bd5", 216 | "prompt-symbol": "#e5c07b", 217 | "prompt-greater-than": "#e5c07b", 218 | }, 219 | "cmd": { 220 | "prompt-path": "#87ceeb ", 221 | "prompt-greater-than": "#aaaaaa", 222 | }, 223 | "docker": { 224 | "prompt-user": "#61afef", 225 | "prompt-host": "#e06c75", 226 | "prompt-path": "#98c379", 227 | }, 228 | "postgres": { 229 | "prompt-db": "#fabd2f", 230 | }, 231 | "global": { 232 | "prompt-at": "#777777", 233 | "prompt-colon": "#777777", 234 | "prompt-dollar": "#777777", 235 | "prompt-hash": "#777777", 236 | "prompt-dash":"#777777", 237 | "prompt-bracket-open": "#777777", 238 | "prompt-bracket-close": "#777777", 239 | "prompt-square-open": "#777777", 240 | "prompt-square-close": "#777777", 241 | "prompt-greater-than": "#777777", 242 | "prompt-symbol": "#888888", 243 | "prompt-user": "#777777", 244 | "prompt-host": "#777777", 245 | "prompt-path": "#777777", 246 | "prompt-branch": "#777777", 247 | "prompt-db": "#777777", 248 | "prompt-zsh-symbol": "#777777", 249 | "prompt-zsh-status-error": "#777777", 250 | "prompt-zsh-status-ok": "#777777", 251 | "prompt-kali-symbol": "#777777", 252 | "prompt-percent": "#777777" 253 | } 254 | }; 255 | 256 | const SolarizedLightPromptColors: Record> = { 257 | "bash": { 258 | "prompt-user": "#61afef", 259 | "prompt-host": "#e5c07b", 260 | "prompt-path": "#98c379", 261 | }, 262 | "bashalt": { 263 | "prompt-user": "#61afef", 264 | "prompt-host": "#d19a66", 265 | "prompt-path": "#56b6c2", 266 | "prompt-hash": "#ff5555", 267 | }, 268 | "kali": { 269 | "prompt-user": "#2679F2", 270 | "prompt-host": "#2679F2", 271 | "prompt-path": "#586e75", 272 | "prompt-kali-symbol": "#2679F2", 273 | "prompt-dollar": "#2679F2", 274 | "prompt-dash": "#56AA9B", 275 | "prompt-bracket-open": "#56AA9B", 276 | "prompt-bracket-close": "#56AA9B", 277 | "prompt-square-open": "#56AA9B", 278 | "prompt-square-close": "#56AA9B", 279 | }, 280 | "zshgit": { 281 | "prompt-path": "#61afef", 282 | "prompt-branch": "#c678dd", 283 | "prompt-zsh-status-error": "#ff5555", 284 | "prompt-zsh-status-ok": "#50fa7b", 285 | "prompt-zsh-symbol": "#00ff00", 286 | "prompt-symbol": "#8be9fd", 287 | }, 288 | "zsh": { 289 | "prompt-user": "#56b6c2", 290 | "prompt-host": "#e06c75", 291 | "prompt-path": "#98c379", 292 | "prompt-percent": "#abb2bf", 293 | }, 294 | "fish": { 295 | "prompt-path": "#61afef", 296 | }, 297 | "ps": { 298 | "prompt-path": "#5b9bd5", 299 | "prompt-symbol": "#e5c07b", 300 | "prompt-greater-than": "#e5c07b", 301 | }, 302 | "cmd": { 303 | "prompt-path": "#87ceeb ", 304 | "prompt-greater-than": "#aaaaaa", 305 | }, 306 | "docker": { 307 | "prompt-user": "#61afef", 308 | "prompt-host": "#e06c75", 309 | "prompt-path": "#98c379", 310 | }, 311 | "postgres": { 312 | "prompt-db": "#fabd2f", 313 | }, 314 | "global": { 315 | "prompt-at": "#777777", 316 | "prompt-colon": "#777777", 317 | "prompt-dollar": "#777777", 318 | "prompt-hash": "#777777", 319 | "prompt-dash":"#777777", 320 | "prompt-bracket-open": "#777777", 321 | "prompt-bracket-close": "#777777", 322 | "prompt-square-open": "#777777", 323 | "prompt-square-close": "#777777", 324 | "prompt-greater-than": "#777777", 325 | "prompt-symbol": "#888888", 326 | "prompt-user": "#777777", 327 | "prompt-host": "#777777", 328 | "prompt-path": "#777777", 329 | "prompt-branch": "#777777", 330 | "prompt-db": "#777777", 331 | "prompt-zsh-symbol": "#777777", 332 | "prompt-zsh-status-error": "#777777", 333 | "prompt-zsh-status-ok": "#777777", 334 | "prompt-kali-symbol": "#777777", 335 | "prompt-percent": "#777777" 336 | } 337 | }; 338 | 339 | const ObsidianLightPromptPromptColors: Record> = { 340 | "bash": { 341 | "prompt-user": "#61afef", 342 | "prompt-host": "#e5c07b", 343 | "prompt-path": "#98c379", 344 | }, 345 | "bashalt": { 346 | "prompt-user": "#61afef", 347 | "prompt-host": "#d19a66", 348 | "prompt-path": "#56b6c2", 349 | "prompt-hash": "#ff5555", 350 | }, 351 | "kali": { 352 | "prompt-user": "#2679F2", 353 | "prompt-host": "#2679F2", 354 | "prompt-path": "#5c6370", 355 | "prompt-kali-symbol": "#2679F2", 356 | "prompt-dollar": "#2679F2", 357 | "prompt-dash": "#56AA9B", 358 | "prompt-bracket-open": "#56AA9B", 359 | "prompt-bracket-close": "#56AA9B", 360 | "prompt-square-open": "#56AA9B", 361 | "prompt-square-close": "#56AA9B", 362 | }, 363 | "zshgit": { 364 | "prompt-path": "#61afef", 365 | "prompt-branch": "#c678dd", 366 | "prompt-zsh-status-error": "#ff5555", 367 | "prompt-zsh-status-ok": "#50fa7b", 368 | "prompt-zsh-symbol": "#00ff00", 369 | "prompt-symbol": "#8be9fd", 370 | }, 371 | "zsh": { 372 | "prompt-user": "#56b6c2", 373 | "prompt-host": "#e06c75", 374 | "prompt-path": "#98c379", 375 | "prompt-percent": "#abb2bf", 376 | }, 377 | "fish": { 378 | "prompt-path": "#61afef", 379 | }, 380 | "ps": { 381 | "prompt-path": "#5b9bd5", 382 | "prompt-symbol": "#e5c07b", 383 | "prompt-greater-than": "#e5c07b", 384 | }, 385 | "cmd": { 386 | "prompt-path": "#87ceeb ", 387 | "prompt-greater-than": "#aaaaaa", 388 | }, 389 | "docker": { 390 | "prompt-user": "#61afef", 391 | "prompt-host": "#e06c75", 392 | "prompt-path": "#98c379", 393 | }, 394 | "postgres": { 395 | "prompt-db": "#fabd2f", 396 | }, 397 | "global": { 398 | "prompt-at": "#777777", 399 | "prompt-colon": "#777777", 400 | "prompt-dollar": "#777777", 401 | "prompt-hash": "#777777", 402 | "prompt-dash":"#777777", 403 | "prompt-bracket-open": "#777777", 404 | "prompt-bracket-close": "#777777", 405 | "prompt-square-open": "#777777", 406 | "prompt-square-close": "#777777", 407 | "prompt-greater-than": "#777777", 408 | "prompt-symbol": "#888888", 409 | "prompt-user": "#777777", 410 | "prompt-host": "#777777", 411 | "prompt-path": "#777777", 412 | "prompt-branch": "#777777", 413 | "prompt-db": "#777777", 414 | "prompt-zsh-symbol": "#777777", 415 | "prompt-zsh-status-error": "#777777", 416 | "prompt-zsh-status-ok": "#777777", 417 | "prompt-kali-symbol": "#777777", 418 | "prompt-percent": "#777777" 419 | } 420 | }; 421 | 422 | export const RootPromptColors: Record> = { 423 | "bash": { 424 | "prompt-user": "#e63946", 425 | "prompt-host": "#e5c07b", 426 | "prompt-path": "#ffb347", 427 | "prompt-hash": "#ff5555", 428 | }, 429 | "bashalt": { 430 | "prompt-user": "#e63946", 431 | "prompt-host": "#d19a66", 432 | "prompt-path": "#ffb347", 433 | "prompt-hash": "#ff5555", 434 | }, 435 | "kali": { 436 | "prompt-user": "#e63946", 437 | "prompt-host": "#e63946", 438 | "prompt-path": "#ffb347", 439 | "prompt-kali-symbol": "#e63946", 440 | "prompt-hash": "#ff5555", 441 | "prompt-dash":"#3370D7", 442 | "prompt-bracket-open": "#3370D7", 443 | "prompt-bracket-close": "#3370D7", 444 | "prompt-square-open": "#3370D7", 445 | "prompt-square-close": "#3370D7", 446 | }, 447 | "zsh": { 448 | "prompt-user": "#e63946", 449 | "prompt-path": "#ffb347", 450 | "prompt-hash": "#ff5555", 451 | "prompt-end": "#ff5555", 452 | }, 453 | "docker": { 454 | "prompt-user": "#e63946", 455 | "prompt-container": "#e06c75", 456 | "prompt-path": "#ffb347", 457 | "prompt-hash": "#ff5555", 458 | }, 459 | "global": {} 460 | }; 461 | 462 | /*const ObsidianPromptColors: Record> = { 463 | "bash": { 464 | "prompt-user": "#5c99f5", 465 | "prompt-host": "#b3b3b3", 466 | "prompt-path": "#86b300", 467 | }, 468 | "bashalt": { 469 | "prompt-user": "#e63946", 470 | "prompt-host": "#d19a66", 471 | "prompt-path": "#56b6c2", 472 | "prompt-hash": "#cb4b16", 473 | }, 474 | "kali": { 475 | "prompt-user": "#ff5555", 476 | "prompt-host": "#ff79c6", 477 | "prompt-path": "#8be9fd", 478 | "prompt-kali-symbol": "#5c99f5", 479 | "prompt-dollar": "#5c99f5", 480 | }, 481 | "zshgit": { 482 | "prompt-path": "#5c99f5", 483 | "prompt-branch": "#c678dd", 484 | "prompt-zsh-error": "#e06c75", 485 | "prompt-zsh-symbol": "#86b300", 486 | }, 487 | "ps": { 488 | "prompt-path": "#5294e2", 489 | }, 490 | "cmd": { 491 | "prompt-path": "#3FC1FF", 492 | }, 493 | "docker": { 494 | "prompt-user": "#5c99f5", 495 | "prompt-host": "#b3b3b3", 496 | "prompt-path": "#86b300", 497 | }, 498 | "postgres": { 499 | "prompt-db": "#d19a66", 500 | }, 501 | "global": { 502 | "prompt-at": "#999999", 503 | "prompt-colon": "#999999", 504 | "prompt-dollar": "#aaaaaa", 505 | "prompt-hash": "#aaaaaa", 506 | "prompt-bracket-open": "#999999", 507 | "prompt-bracket-close": "#999999", 508 | "prompt-square-open": "#999999", 509 | "prompt-square-close": "#999999", 510 | "prompt-greater-than": "#999999", 511 | } 512 | };*/ 513 | 514 | const SolarizedDarkColors = { 515 | codeblock: { 516 | activeLineColor: D_ACTIVE_CODEBLOCK_LINE_COLOR, 517 | backgroundColor: D_BACKGROUND_COLOR, 518 | highlightColor: D_HIGHLIGHT_COLOR, 519 | alternateHighlightColors: {}, 520 | languageBorderColors: {}, 521 | textColor: '#A30505', 522 | bracketHighlightColorMatch: '#36e920', 523 | bracketHighlightColorNoMatch: '#FF0000', 524 | bracketHighlightBackgroundColorMatch: D_ACTIVE_CODEBLOCK_LINE_COLOR, 525 | bracketHighlightBackgroundColorNoMatch:D_ACTIVE_CODEBLOCK_LINE_COLOR, 526 | selectionMatchHighlightColor: SELECTION_MATCH_COLOR, 527 | }, 528 | header: { 529 | backgroundColor: D_HEADER_COLOR, 530 | textColor: D_HEADER_TEXT_COLOR, 531 | lineColor: D_HEADER_LINE_COLOR, 532 | codeBlockLangTextColor: D_LANG_COLOR, 533 | codeBlockLangBackgroundColor: D_LANG_BACKGROUND_COLOR, 534 | }, 535 | gutter: { 536 | textColor: D_GUTTER_TEXT_COLOR, 537 | backgroundColor: D_GUTTER_BACKGROUND_COLOR, 538 | activeLineNrColor: D_GUTTER_ACTIVE_LINENR_COLOR, 539 | }, 540 | inlineCode: { 541 | backgroundColor: D_INLINE_CODE_BACKGROUND_COLOR, 542 | textColor: D_INLINE_CODE_TEXT_COLOR, 543 | }, 544 | prompts: { 545 | promptColors: DarkPromptColors, 546 | rootPromptColors: RootPromptColors, 547 | editedPromptColors: {}, 548 | editedRootPromptColors: {} 549 | }, 550 | groupedCodeBlocks: { 551 | activeTabBackgroundColor: '#B58900', 552 | hoverTabBackgroundColor: '#00AAAA', 553 | hoverTabTextColor: '#FFFFFF', 554 | }, 555 | editorActiveLineColor: D_ACTIVE_LINE_COLOR, 556 | languageSpecificColors: {}, 557 | } 558 | 559 | const SolarizedLightColors = { 560 | codeblock: { 561 | activeLineColor: L_ACTIVE_CODEBLOCK_LINE_COLOR, 562 | backgroundColor: L_BACKGROUND_COLOR, 563 | highlightColor: L_HIGHLIGHT_COLOR, 564 | alternateHighlightColors: {}, 565 | languageBorderColors: {}, 566 | textColor: '#A30505', 567 | bracketHighlightColorMatch: '#ff01f7', 568 | bracketHighlightColorNoMatch: '#FF0000', 569 | bracketHighlightBackgroundColorMatch: L_ACTIVE_CODEBLOCK_LINE_COLOR, 570 | bracketHighlightBackgroundColorNoMatch:L_ACTIVE_CODEBLOCK_LINE_COLOR, 571 | selectionMatchHighlightColor: SELECTION_MATCH_COLOR, 572 | }, 573 | header: { 574 | backgroundColor: L_HEADER_COLOR, 575 | textColor: L_HEADER_TEXT_COLOR, 576 | lineColor: L_HEADER_LINE_COLOR, 577 | codeBlockLangTextColor: L_LANG_COLOR, 578 | codeBlockLangBackgroundColor: L_LANG_BACKGROUND_COLOR, 579 | }, 580 | gutter: { 581 | textColor: L_GUTTER_TEXT_COLOR, 582 | backgroundColor: L_GUTTER_BACKGROUND_COLOR, 583 | activeLineNrColor: L_GUTTER_ACTIVE_LINENR_COLOR, 584 | }, 585 | inlineCode: { 586 | backgroundColor: L_INLINE_CODE_BACKGROUND_COLOR, 587 | textColor: L_INLINE_CODE_TEXT_COLOR, 588 | }, 589 | prompts: { 590 | promptColors: SolarizedLightPromptColors, 591 | rootPromptColors: RootPromptColors, 592 | editedPromptColors: {}, 593 | editedRootPromptColors: {} 594 | }, 595 | groupedCodeBlocks: { 596 | activeTabBackgroundColor: '#FFD700', 597 | hoverTabBackgroundColor: '#A6A18F',//'#CFCAB3', 598 | hoverTabTextColor: '#C25F30', 599 | }, 600 | editorActiveLineColor: L_ACTIVE_LINE_COLOR, 601 | languageSpecificColors: {}, 602 | } 603 | 604 | const Solarized: Theme = { 605 | baseTheme: "Solarized", 606 | settings: { 607 | codeblock: { 608 | enableLineNumbers: true, 609 | enableActiveLineHighlight: true, 610 | codeBlockBorderStylingPosition: 'disable', 611 | showIndentationLines: false, 612 | enableLinks: false, 613 | enableLinkUpdate: false, 614 | enableBracketHighlight: true, 615 | highlightNonMatchingBrackets: true, 616 | inverseFold: false, 617 | enableSelectionMatching: false, 618 | unwrapcode: false, 619 | buttons: { 620 | alwaysShowButtons: false, 621 | alwaysShowCopyCodeButton: false, 622 | enableSelectCodeButton: false, 623 | enableDeleteCodeButton: false, 624 | enableWrapCodeButton: false, 625 | }, 626 | }, 627 | textHighlight: { 628 | lineSeparator: '', 629 | textSeparator: '', 630 | }, 631 | semiFold: { 632 | enableSemiFold: false, 633 | visibleLines: 5, 634 | showAdditionalUncollapseButon: false, 635 | }, 636 | header: { 637 | boldText: false, 638 | italicText: false, 639 | collapseIconPosition: 'hide', 640 | collapsedCodeText: '', 641 | codeblockLangBoldText: true, 642 | codeblockLangItalicText: true, 643 | alwaysDisplayCodeblockLang: false, 644 | alwaysDisplayCodeblockIcon: false, 645 | displayCodeBlockLanguage: true, 646 | displayCodeBlockIcon: false, 647 | disableFoldUnlessSpecified: false, 648 | }, 649 | gutter: { 650 | highlightActiveLineNr: false, 651 | enableHighlight: false, 652 | }, 653 | inlineCode: { 654 | enableInlineCodeStyling: false, 655 | }, 656 | printing: { 657 | enablePrintToPDFStyling: true, 658 | forceCurrentColorUse: false, 659 | uncollapseDuringPrint: true, 660 | }, 661 | common: { 662 | enableInSourceMode: false, 663 | }, 664 | prompts: { 665 | editedDefaults: {}, 666 | customPrompts: {} 667 | }, 668 | enableEditorActiveLineHighlight: true, 669 | }, 670 | colors: { 671 | dark: SolarizedDarkColors, 672 | light: SolarizedLightColors, 673 | }, 674 | } 675 | 676 | const ObsidianDarkColors = { 677 | codeblock: { 678 | activeLineColor: "--color-base-30", 679 | backgroundColor: "--code-background", 680 | highlightColor: "--text-highlight-bg", 681 | alternateHighlightColors: {}, 682 | languageBorderColors: {}, 683 | textColor: '#A30505', 684 | bracketHighlightColorMatch: '#f33bff', 685 | bracketHighlightColorNoMatch: '#FF0000', 686 | bracketHighlightBackgroundColorMatch: "--color-base-30", 687 | bracketHighlightBackgroundColorNoMatch: "--color-base-30", 688 | selectionMatchHighlightColor: SELECTION_MATCH_COLOR, 689 | }, 690 | header: { 691 | backgroundColor: "--code-background", 692 | textColor: "--text-normal", 693 | lineColor: "--color-base-30", 694 | codeBlockLangTextColor: "--code-comment", 695 | codeBlockLangBackgroundColor: "--code-background", 696 | }, 697 | gutter: { 698 | textColor: "--text-faint", 699 | backgroundColor: "--code-background", 700 | activeLineNrColor: "--text-muted", 701 | }, 702 | inlineCode: { 703 | backgroundColor: "--code-background", 704 | textColor: "--code-normal", 705 | }, 706 | prompts: { 707 | promptColors: DarkPromptColors, 708 | rootPromptColors: RootPromptColors, 709 | editedPromptColors: {}, 710 | editedRootPromptColors: {} 711 | }, 712 | groupedCodeBlocks: { 713 | activeTabBackgroundColor: '#3A3A3A', 714 | hoverTabBackgroundColor: '#333333', 715 | hoverTabTextColor: '#CCCCCC', 716 | }, 717 | editorActiveLineColor: "--color-base-20", 718 | languageSpecificColors: {}, 719 | } 720 | 721 | const ObsidianLightColors = { 722 | codeblock: { 723 | activeLineColor: "--color-base-30", 724 | backgroundColor: "--code-background", 725 | highlightColor: "--text-highlight-bg", 726 | alternateHighlightColors: {}, 727 | languageBorderColors: {}, 728 | textColor: '#A30505', 729 | bracketHighlightColorMatch: '#f33bff', 730 | bracketHighlightColorNoMatch: '#FF0000', 731 | bracketHighlightBackgroundColorMatch: "--color-base-30", 732 | bracketHighlightBackgroundColorNoMatch: "--color-base-30", 733 | selectionMatchHighlightColor: SELECTION_MATCH_COLOR, 734 | }, 735 | header: { 736 | backgroundColor: "--code-background", 737 | textColor: "--text-normal", 738 | lineColor: "--color-base-30", 739 | codeBlockLangTextColor: "--code-comment", 740 | codeBlockLangBackgroundColor: "--code-background", 741 | }, 742 | gutter: { 743 | textColor: "--text-faint", 744 | backgroundColor: "--code-background", 745 | activeLineNrColor: "--text-muted", 746 | }, 747 | inlineCode: { 748 | backgroundColor: "--code-background", 749 | textColor: "--code-normal", 750 | }, 751 | prompts: { 752 | promptColors: ObsidianLightPromptPromptColors, 753 | rootPromptColors: RootPromptColors, 754 | editedPromptColors: {}, 755 | editedRootPromptColors: {} 756 | }, 757 | groupedCodeBlocks: { 758 | activeTabBackgroundColor: '#E6E6E6', 759 | hoverTabBackgroundColor: '#F0F0F0', 760 | hoverTabTextColor: '#888888', 761 | }, 762 | editorActiveLineColor: "--color-base-20", 763 | languageSpecificColors: {}, 764 | } 765 | 766 | const Obsidian: Theme = { 767 | baseTheme: "Obsidian", 768 | settings: { 769 | codeblock: { 770 | enableLineNumbers: true, 771 | enableActiveLineHighlight: true, 772 | codeBlockBorderStylingPosition: 'disable', 773 | showIndentationLines: false, 774 | enableLinks: false, 775 | enableLinkUpdate: false, 776 | enableBracketHighlight: true, 777 | highlightNonMatchingBrackets: true, 778 | inverseFold: false, 779 | enableSelectionMatching: false, 780 | unwrapcode: false, 781 | buttons: { 782 | alwaysShowButtons: false, 783 | alwaysShowCopyCodeButton: false, 784 | enableSelectCodeButton: false, 785 | enableDeleteCodeButton: false, 786 | enableWrapCodeButton: false, 787 | }, 788 | }, 789 | textHighlight: { 790 | lineSeparator: '', 791 | textSeparator: '', 792 | }, 793 | semiFold: { 794 | enableSemiFold: false, 795 | visibleLines: 5, 796 | showAdditionalUncollapseButon: false, 797 | }, 798 | header: { 799 | boldText: false, 800 | italicText: false, 801 | collapseIconPosition: 'hide', 802 | collapsedCodeText: '', 803 | codeblockLangBoldText: true, 804 | codeblockLangItalicText: true, 805 | alwaysDisplayCodeblockLang: false, 806 | alwaysDisplayCodeblockIcon: false, 807 | displayCodeBlockLanguage: true, 808 | displayCodeBlockIcon: false, 809 | disableFoldUnlessSpecified: false, 810 | }, 811 | gutter: { 812 | highlightActiveLineNr: true, 813 | enableHighlight: true, 814 | }, 815 | inlineCode: { 816 | enableInlineCodeStyling: false, 817 | }, 818 | printing: { 819 | enablePrintToPDFStyling: true, 820 | forceCurrentColorUse: false, 821 | uncollapseDuringPrint: true, 822 | }, 823 | common: { 824 | enableInSourceMode: false, 825 | }, 826 | prompts: { 827 | editedDefaults: {}, 828 | customPrompts: {} 829 | }, 830 | enableEditorActiveLineHighlight: true, 831 | }, 832 | colors: { 833 | dark: ObsidianDarkColors, 834 | light: ObsidianLightColors, 835 | }, 836 | } 837 | 838 | export const DEFAULT_THEMES = { 839 | 'Obsidian': Obsidian, 840 | 'Solarized': Solarized, 841 | } 842 | 843 | export const DEFAULT_SETTINGS: CodeblockCustomizerSettings = { 844 | Themes: structuredClone(DEFAULT_THEMES), 845 | ExcludeLangs: 'dataview, ad-*', 846 | SelectedTheme: structuredClone(Obsidian), 847 | ThemeName: "Obsidian", 848 | newThemeName: "", 849 | newPromptName: "", 850 | alternateHighlightColorName: "", 851 | languageBorderColorName: "", 852 | foldAllCommand: false, 853 | settingsType: "basic", 854 | langSpecificSettingsType: "", 855 | languageSpecificLanguageName: "", 856 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Plugin, MarkdownView, WorkspaceLeaf, TAbstractFile, TFile, getLinkpath, Vault, Notice, Editor } from "obsidian"; 2 | import { Extension, StateField } from "@codemirror/state"; 3 | import { EditorView, DecorationSet } from "@codemirror/view"; 4 | import * as _ from 'lodash'; 5 | import { DEFAULT_SETTINGS, CodeblockCustomizerSettings } from './Settings'; 6 | import { ReadingView, calloutPostProcessor, convertHTMLCollectionToArray, foldAllReadingView, toggleFoldClasses } from "./ReadingView"; 7 | import { SettingsTab } from "./SettingsTab"; 8 | import { loadIcons, BLOBS, updateSettingStyles, mergeBorderColorsToLanguageSpecificColors, loadSyntaxHighlightForCustomLanguages, customLanguageConfig, getFileCacheAndContentLines, indentCodeBlock, unIndentCodeBlock} from "./Utils"; 9 | import { CodeBlockPositions, extensions, updateValue } from "./EditorExtensions"; 10 | import { GroupedCodeBlockRenderChild } from "./GroupedCodeBlockRenderer"; 11 | // npm i @simonwep/pickr 12 | 13 | interface codeBlock { 14 | codeBlockText: string; 15 | from: number; 16 | to: number; 17 | } 18 | 19 | export default class CodeBlockCustomizerPlugin extends Plugin { 20 | settings: CodeblockCustomizerSettings; 21 | extensions: Extension[]; 22 | theme: string; 23 | editorExtensions: { extensions: (StateField | StateField | Extension)[]; 24 | foldAll: (view: EditorView, settings: CodeblockCustomizerSettings, fold: boolean, defaultState: boolean) => void; 25 | customBracketMatching: Extension; 26 | selectionMatching: Extension; 27 | } 28 | customLanguageConfig: customLanguageConfig | null; 29 | groupedChildrenMap: Map; 30 | activeEditorTabs: Map> = new Map(); 31 | 32 | async onload() { 33 | document.body.classList.add('codeblock-customizer'); 34 | await this.loadSettings(); 35 | updateSettingStyles(this.settings, this.app); 36 | 37 | this.extensions = []; 38 | this.customLanguageConfig = null; 39 | // npm install eslint@8.39.0 -g 40 | // eslint main.ts 41 | 42 | this.groupedChildrenMap = new Map(); 43 | 44 | /* Problems to solve: 45 | - if a language is excluded then: 46 | - header needs to unfold before removing it, 47 | */ 48 | 49 | // add fold all command 50 | this.addCommand({ 51 | id: 'codeblock-customizer-foldall-editor', 52 | name: 'Fold all code blocks', 53 | callback: () => { 54 | const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView); 55 | if (markdownView) { 56 | // @ts-ignore 57 | const mode = markdownView.currentMode.type; 58 | document.body.classList.add('codeblock-customizer-header-collapse-command'); 59 | this.settings.foldAllCommand = true; 60 | if (mode === "source") { 61 | // @ts-ignore 62 | this.editorExtensions.foldAll(markdownView.editor.cm, this.settings, true, false); 63 | foldAllReadingView(true, this.settings); 64 | } else if (mode === "preview") { 65 | foldAllReadingView(true, this.settings); 66 | } 67 | } 68 | } 69 | }); 70 | 71 | // add unfold all command 72 | this.addCommand({ 73 | id: 'codeblock-customizer-unfoldall-editor', 74 | name: 'Unfold all code blocks', 75 | callback: () => { 76 | const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView); 77 | if (markdownView) { 78 | // @ts-ignore 79 | const mode = markdownView.currentMode.type; 80 | document.body.classList.add('codeblock-customizer-header-collapse-command'); 81 | this.settings.foldAllCommand = true; 82 | if (mode === "source") { 83 | // @ts-ignore 84 | this.editorExtensions.foldAll(markdownView.editor.cm, this.settings, false, false); 85 | foldAllReadingView(false, this.settings); 86 | } else if (mode === "preview") { 87 | foldAllReadingView(false, this.settings); 88 | } 89 | } 90 | } 91 | }); 92 | 93 | // restore default state 94 | this.addCommand({ 95 | id: 'codeblock-customizer-restore-fold-editor', 96 | name: 'Restore folding state of all code blocks to default', 97 | callback: () => { 98 | const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView); 99 | if (markdownView) { 100 | // @ts-ignore 101 | const mode = markdownView.currentMode.type; 102 | document.body.classList.remove('codeblock-customizer-header-collapse-command'); 103 | this.settings.foldAllCommand = false; 104 | if (mode === "source") { 105 | // @ts-ignore 106 | this.editorExtensions.foldAll(markdownView.editor.cm, this.settings, true, false); 107 | // @ts-ignore 108 | this.editorExtensions.foldAll(markdownView.editor.cm, this.settings, false, true); 109 | foldAllReadingView(false, this.settings); 110 | this.restoreDefaultFold(); 111 | } else if (mode === "preview") { 112 | foldAllReadingView(false, this.settings); 113 | this.restoreDefaultFold(); 114 | } 115 | } 116 | } 117 | }); 118 | 119 | // indent code block 120 | this.addCommand({ 121 | id: 'codeblock-customizer-indent-codeblock', 122 | name: 'Indent code block by one level', 123 | editorCallback: async (editor: Editor, view: MarkdownView) => { 124 | indentCodeBlock(editor, view); 125 | } 126 | }); 127 | 128 | // unindent code block 129 | this.addCommand({ 130 | id: 'codeblock-customizer-unindent-codeblock', 131 | name: 'Unindent code block by one level', 132 | editorCallback: async (editor: Editor, view: MarkdownView) => { 133 | unIndentCodeBlock(editor, view); 134 | } 135 | }); 136 | 137 | await loadIcons(this); 138 | loadSyntaxHighlightForCustomLanguages(this); // load syntax highlight 139 | 140 | mergeBorderColorsToLanguageSpecificColors(this, this.settings); 141 | 142 | this.editorExtensions = extensions(this, this.settings); 143 | this.registerEditorExtension(this.editorExtensions.extensions); 144 | 145 | if (this.settings.SelectedTheme.settings.codeblock.enableBracketHighlight) { 146 | this.extensions.push(this.editorExtensions.customBracketMatching); 147 | } 148 | if (this.settings.SelectedTheme.settings.codeblock.enableSelectionMatching) { 149 | this.extensions.push(this.editorExtensions.selectionMatching); 150 | } 151 | 152 | this.registerEditorExtension(this.extensions); 153 | 154 | const settingsTab = new SettingsTab(this.app, this); 155 | this.addSettingTab(settingsTab); 156 | if (this.settings.ThemeName == "") { 157 | this.updateTheme(settingsTab); 158 | } else { 159 | updateSettingStyles(this.settings, this.app); 160 | } 161 | 162 | this.registerEvent(this.app.workspace.on('css-change', this.handleCssChange.bind(this, settingsTab), this)); 163 | 164 | this.registerEvent(this.app.vault.on('rename', (file: TAbstractFile, oldPath: string) => { 165 | if (this.settings.SelectedTheme.settings.codeblock.enableLinks && this.settings.SelectedTheme.settings.codeblock.enableLinkUpdate) { 166 | this.handleFileRename(file, oldPath); // until Obsidian doesn't adds code block links to metadatacache 167 | } 168 | }, this)); 169 | 170 | // reading mode 171 | this.registerMarkdownPostProcessor(async (el, ctx) => { 172 | await ReadingView(el, ctx, this) 173 | }); 174 | 175 | // process existing open preview views when the plugin loads 176 | this.app.workspace.onLayoutReady(() => { 177 | this.app.workspace.iterateAllLeaves((leaf: WorkspaceLeaf) => { 178 | if (leaf.view instanceof MarkdownView && leaf.view.getMode() === 'preview') { 179 | this.registerGroupedRenderChildForView(leaf.view); 180 | } 181 | }); 182 | }); 183 | 184 | // process new active leaves (e.g. note switches) 185 | this.registerEvent(this.app.workspace.on('active-leaf-change', (leaf: WorkspaceLeaf) => { 186 | if (leaf && leaf.view instanceof MarkdownView && leaf.view.getMode() === 'preview') { 187 | this.registerGroupedRenderChildForView(leaf.view); 188 | } 189 | })); 190 | 191 | // process layout-change (editor mode -> reading mode) 192 | this.registerEvent(this.app.workspace.on('layout-change', () => { 193 | const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView); 194 | if (markdownView && markdownView.getMode() === 'preview') { 195 | this.registerGroupedRenderChildForView(markdownView); 196 | } 197 | })); 198 | 199 | this.registerMarkdownPostProcessor(async (el, ctx) => { 200 | await calloutPostProcessor(el, ctx, this) 201 | }); 202 | 203 | this.app.workspace.onLayoutReady(() => { 204 | this.renderReadingViewOnStart(); 205 | }); 206 | 207 | console.log("loading CodeBlock Customizer plugin"); 208 | }// onload 209 | 210 | handleCssChange(settingsTab: SettingsTab) { 211 | this.updateTheme(settingsTab); 212 | }// handleCssChange 213 | 214 | updateTheme(settingsTab: SettingsTab) { 215 | settingsTab.applyTheme(); 216 | this.saveSettings(); 217 | }// updateTheme 218 | 219 | onunload() { 220 | console.log("unloading CodeBlock Customizer plugin"); 221 | 222 | // remove GroupedCodeBlockRenderChild 223 | this.groupedChildrenMap.forEach((child, view) => { 224 | view.removeChild(child); // onunload() 225 | }); 226 | this.groupedChildrenMap.clear(); 227 | 228 | // unload icons 229 | for (const url of Object.values(BLOBS)) { 230 | URL.revokeObjectURL(url) 231 | } 232 | 233 | // unload syntax highlight 234 | loadSyntaxHighlightForCustomLanguages(this, true); 235 | }// onunload 236 | 237 | registerGroupedRenderChildForView(markdownView: MarkdownView) { 238 | if (!markdownView || !markdownView.containerEl) { 239 | return; 240 | } 241 | 242 | const child = this.groupedChildrenMap.get(markdownView); 243 | 244 | if (child) { 245 | //console.log("Existing GroupedCodeBlockRenderChild found for this view. Re-processing."); 246 | // if the view already has the child, just tell it to re-process its content 247 | child.processGroupedCodeBlocks(); // Make sure this method is public in GroupedCodeBlockRenderChild 248 | } else { 249 | // create a new child if one doesn't exist for this view 250 | const renderChild = new GroupedCodeBlockRenderChild(markdownView.containerEl, markdownView, this.groupedChildrenMap, this); 251 | markdownView.addChild(renderChild); 252 | this.groupedChildrenMap.set(markdownView, renderChild); 253 | //console.log("Registered NEW GroupedCodeBlockRenderChild for view:", markdownView); 254 | } 255 | }// registerGroupedRenderChildForView 256 | 257 | async handleFileRename(file: TAbstractFile, oldPath: string) { 258 | const markdownFiles = this.app.vault.getMarkdownFiles(); 259 | let linkUpdateCount = 0; 260 | let fileCount = 0; 261 | 262 | for (const mdFile of markdownFiles) { 263 | let linkUpdate = 0; 264 | const { cache, fileContentLines } = await getFileCacheAndContentLines(this, mdFile.path); 265 | if (!cache || !fileContentLines) 266 | continue; 267 | 268 | if (cache?.sections) { 269 | const codeBlocks: codeBlock[] = []; 270 | for (const sections of cache.sections) { 271 | if (sections.type === "code") { 272 | const codeBlockLines = fileContentLines.slice(sections.position.start.line, sections.position.end.line + 1); 273 | const codeBlockText = codeBlockLines.join('\n'); 274 | codeBlocks.push({codeBlockText, from: sections.position.start.line, to: sections.position.end.line}); 275 | } 276 | } 277 | for (const codeBlock of codeBlocks) { 278 | const ret = this.findAllCodeBlockLinks(mdFile, codeBlock, oldPath, file); 279 | linkUpdateCount += ret; 280 | if (ret > 0) { 281 | linkUpdate++; 282 | } 283 | } 284 | } 285 | if (linkUpdate > 0) { 286 | fileCount++; 287 | } 288 | } 289 | if (linkUpdateCount > 0) { 290 | new Notice(`Updated ${linkUpdateCount} code block links in ${fileCount} files.`); 291 | } 292 | }// handleFileRename 293 | 294 | findAllCodeBlockLinks(currentFile: TFile, currentCodeBlock: codeBlock, oldPath: string, newPath: TAbstractFile) { 295 | const linkRegex = /\[\[(.*?)\]\]/g; 296 | const matches: IterableIterator = currentCodeBlock.codeBlockText.matchAll(linkRegex); 297 | let modifiedCodeBlockText = currentCodeBlock.codeBlockText; 298 | let linkUpdateCount = 0; 299 | 300 | if (!matches) { 301 | return 0; 302 | } 303 | 304 | for (const match of matches) { 305 | const { updatedCodeBlockText: updatedText, updated } = this.updateCodeBlockContent(match, currentFile, oldPath, newPath, modifiedCodeBlockText); 306 | modifiedCodeBlockText = updatedText; 307 | if (updated) { 308 | linkUpdateCount++; 309 | } 310 | } 311 | if (modifiedCodeBlockText !== currentCodeBlock.codeBlockText) { 312 | this.updateLinksInFiles(this.app.vault, currentFile, currentCodeBlock.from, currentCodeBlock.to, modifiedCodeBlockText.split('\n')); 313 | } 314 | return linkUpdateCount; 315 | }// findAllCodeBlockLinks 316 | 317 | updateCodeBlockContent(match: RegExpMatchArray, currentFile: TFile, oldPath: string, newPath: TAbstractFile, updatedCodeBlockText: string) { 318 | const linkText = match[1]; 319 | const displayNameRef = this.getDisplayNameAndReference(linkText); 320 | const linkTextWithoutDisplayName = linkText.split('|')[0].split('#')[0]; // Remove DisplayName 321 | const oldPathWithoutExtension = oldPath.replace(/\.[^.]*$/, ''); // Remove extension 322 | const oldPathWithoutDir = oldPath.split('/').slice(-1)[0]; // Extract last segment after '/' 323 | const oldPathWithoutExtensionAndDir = oldPathWithoutDir.replace(/\.[^.]*$/, ''); // Remove extension from last segment 324 | const linkPath = this.app.metadataCache.getFirstLinkpathDest(getLinkpath(linkTextWithoutDisplayName), currentFile.path); 325 | // @ts-ignore 326 | const newExtension = '.' + newPath.extension; 327 | const displayNameAndRef = displayNameRef.reference + displayNameRef.displayName; 328 | let updated = false; 329 | 330 | if ((linkTextWithoutDisplayName.contains("/") && linkTextWithoutDisplayName.contains(newExtension)) && linkTextWithoutDisplayName.toLowerCase() === oldPath.toLowerCase()) { // SomeFolder/Untitled 22.md === SomeFolder/Untitled 22.md 331 | if (!linkPath) { 332 | //console.log("(+) Update 1 - In " + currentFile.path + " replace " + oldPath + " with " + newPath.path); 333 | updatedCodeBlockText = updatedCodeBlockText.replace(match[0], '[[' + newPath.path + displayNameAndRef + ']]'); 334 | updated = true; 335 | } 336 | } else if ((!linkTextWithoutDisplayName.contains("/") && linkTextWithoutDisplayName.contains(newExtension)) && linkTextWithoutDisplayName.toLowerCase() === oldPathWithoutDir.toLowerCase()) { // Untitled 22.md === Untitled 22.md 337 | if (!linkPath) { 338 | //console.log("(+) Update 2 - In " + currentFile.path + " replace " + oldPathWithoutDir + " with " + newPath.path); 339 | updatedCodeBlockText = updatedCodeBlockText.replace(match[0], '[[' + newPath.path + displayNameAndRef + ']]'); 340 | updated = true; 341 | } 342 | } else if ((linkTextWithoutDisplayName.contains("/") && !linkTextWithoutDisplayName.contains(newExtension)) && oldPathWithoutExtension.length > 0 && linkTextWithoutDisplayName.toLowerCase() === oldPathWithoutExtension.toLowerCase()) { // SomeFolder/Untitled 22 === SomeFolder/Untitled 22 343 | if (!linkPath) { 344 | //console.log("(+) Update 3 - In " + currentFile.path + " replace " + oldPathWithoutExtension + " with " + newPath.path.replace(/\.[^.]*$/, '')); 345 | updatedCodeBlockText = updatedCodeBlockText.replace(match[0], '[[' + newPath.path.replace(/\.[^.]*$/, '') + displayNameAndRef + ']]'); 346 | updated = true; 347 | } 348 | } else if ((!linkTextWithoutDisplayName.contains("/") && !linkTextWithoutDisplayName.contains(newExtension)) && oldPathWithoutExtensionAndDir.length > 0 && linkTextWithoutDisplayName.toLowerCase() === oldPathWithoutExtensionAndDir.toLowerCase()) { // Untitled 22 === Untitled 22 349 | if (!linkPath) { 350 | //console.log("(+) Update 4 - In " + currentFile.path + " replace " + oldPathWithoutExtensionAndDir + " with " + newPath.path.replace(/\.[^.]*$/, '')); 351 | updatedCodeBlockText = updatedCodeBlockText.replace(match[0], '[[' + newPath.path.replace(/\.[^.]*$/, '') + displayNameAndRef + ']]'); 352 | updated = true; 353 | } 354 | } 355 | 356 | return {updatedCodeBlockText, updated}; 357 | }// updateCodeBlockContent 358 | 359 | async updateLinksInFiles(vault: Vault, file: TFile, startLine: number, endLine: number, newContent: string[]): Promise { 360 | try { 361 | await vault.process(file, (currentContent) => { 362 | const lines = currentContent.split("\n"); 363 | 364 | for (let i = startLine; i <= endLine; i++) { 365 | const index = i - startLine; 366 | lines[i] = newContent[index]; 367 | } 368 | 369 | const modifiedContent = lines.join("\n"); 370 | 371 | return modifiedContent; 372 | }); 373 | } catch (error) { 374 | console.error("Error modifying file:", error); 375 | throw error; 376 | } 377 | }// updateLinksInFiles 378 | 379 | getDisplayNameAndReference(input: string): { displayName: string, reference: string } { 380 | const displayNameMarker = "|"; 381 | const referenceMarker = "#"; 382 | 383 | const displayNameIndex = input.lastIndexOf(displayNameMarker); 384 | const referenceIndex = input.indexOf(referenceMarker); 385 | 386 | const result: { displayName: string, reference: string } = { 387 | displayName: '', 388 | reference: '' 389 | }; 390 | 391 | if (displayNameIndex !== -1) { 392 | result.displayName = input.substring(displayNameIndex); 393 | } 394 | 395 | if (referenceIndex !== -1) { 396 | result.reference = input.substring(referenceIndex, displayNameIndex !== -1 ? displayNameIndex : undefined); 397 | } 398 | 399 | return result; 400 | }// getDisplayNameAndReference 401 | 402 | async loadSettings() { 403 | const loadedData = await this.loadData(); 404 | this.settings = _.merge({}, DEFAULT_SETTINGS, loadedData); // copies new settings to default themes and selectedtheme 405 | 406 | const defaultThemeNames = Object.keys(DEFAULT_SETTINGS.Themes); 407 | const currentThemeNames = Object.keys(this.settings.Themes); 408 | 409 | const userThemeNames = _.difference(currentThemeNames, defaultThemeNames); 410 | 411 | userThemeNames.forEach(themeName => { 412 | const userTheme = this.settings.Themes[themeName]; 413 | const baseThemeName = userTheme.baseTheme; 414 | 415 | if (baseThemeName) { 416 | // copy new settings from corresponding Theme to user themes which do have a baseTheme (created after this change) 417 | const baseTheme = this.settings.Themes[baseThemeName]; 418 | if (baseTheme) { 419 | _.merge(userTheme.settings, baseTheme.settings, userTheme.settings); 420 | _.merge(userTheme.colors, baseTheme.colors, userTheme.colors); 421 | } 422 | } else { 423 | // copy new settings from Obsidian Theme to user themes which do not have a baseTheme (created before this change) 424 | const defaultObsidianSettings = this.settings.Themes["Obsidian"]; 425 | _.merge(userTheme.settings, defaultObsidianSettings.settings, userTheme.settings); 426 | _.merge(userTheme.colors, defaultObsidianSettings.colors, userTheme.colors); 427 | } 428 | }); 429 | 430 | // merge master theme with SelectedTheme 431 | const masterTheme = this.settings.Themes[this.settings.ThemeName]; 432 | const workingCopyTheme = loadedData?.SelectedTheme; 433 | if (masterTheme) { 434 | this.settings.SelectedTheme = _.merge({}, masterTheme, workingCopyTheme); 435 | } 436 | 437 | // prevent bloating, remove unchnged colors 438 | userThemeNames.forEach(themeName => { 439 | const userTheme = this.settings.Themes[themeName]; 440 | userTheme.colors.light.prompts.promptColors = {}; 441 | userTheme.colors.light.prompts.rootPromptColors = {}; 442 | userTheme.colors.dark.prompts.promptColors = {}; 443 | userTheme.colors.dark.prompts.rootPromptColors = {}; 444 | }); 445 | 446 | this.settings.SelectedTheme.colors.light.prompts.promptColors = {}; 447 | this.settings.SelectedTheme.colors.light.prompts.rootPromptColors = {}; 448 | this.settings.SelectedTheme.colors.dark.prompts.promptColors = {}; 449 | this.settings.SelectedTheme.colors.dark.prompts.rootPromptColors = {}; 450 | }// loadSettings 451 | 452 | async saveSettings() { 453 | const clonedSettings = structuredClone(this.settings); 454 | 455 | // Strip base colors before saving to avoid bloat and overwrite 456 | delete clonedSettings.SelectedTheme.colors.light.prompts.promptColors; 457 | delete clonedSettings.SelectedTheme.colors.dark.prompts.promptColors; 458 | delete clonedSettings.SelectedTheme.colors.light.prompts.rootPromptColors; 459 | delete clonedSettings.SelectedTheme.colors.dark.prompts.rootPromptColors; 460 | 461 | await this.saveData(clonedSettings); 462 | updateValue(true); 463 | this.app.workspace.updateOptions(); 464 | updateSettingStyles(this.settings, this.app); 465 | }// saveSettings 466 | 467 | restoreDefaultFold() { 468 | const preElements = document.querySelectorAll('.codeblock-customizer-pre.codeblock-customizer-codeblock-default-collapse'); 469 | preElements.forEach((preElement) => { 470 | //preElement?.classList.add('codeblock-customizer-codeblock-collapsed'); 471 | let lines: Element[] = []; 472 | const codeElements = preElement?.getElementsByTagName("CODE"); 473 | lines = convertHTMLCollectionToArray(codeElements); 474 | toggleFoldClasses(preElement as HTMLPreElement, lines.length, true, this.settings.SelectedTheme.settings.semiFold.enableSemiFold, this.settings.SelectedTheme.settings.semiFold.visibleLines); 475 | }); 476 | }// restoreDefaultFold 477 | 478 | async renderReadingViewOnStart() { 479 | this.app.workspace.iterateRootLeaves((currentLeaf: WorkspaceLeaf) => { 480 | if (currentLeaf.view instanceof MarkdownView) { 481 | const leafMode = currentLeaf.view.getMode(); 482 | if (leafMode === "preview") { 483 | currentLeaf.view.previewMode.rerender(true); 484 | } 485 | } 486 | }); 487 | }// renderReadingViewOnStart 488 | } 489 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES2018", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "isolatedModules": true, 13 | "strictNullChecks": true, 14 | "lib": [ 15 | "DOM", 16 | "ES2018" 17 | ] 18 | }, 19 | "include": [ 20 | "**/*.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.2.0": "0.15.0", 3 | "1.2.1": "0.15.0", 4 | "1.2.2": "0.15.0", 5 | "1.2.3": "0.15.0", 6 | "1.2.4": "0.15.0", 7 | "1.2.5": "0.15.0", 8 | "1.2.6": "0.15.0", 9 | "1.2.7": "0.15.0", 10 | "1.2.8": "0.15.0" 11 | } 12 | --------------------------------------------------------------------------------