├── .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 |
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 aelement, 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 | --------------------------------------------------------------------------------