├── .gitignore ├── LICENSE ├── README.md ├── changelist.md ├── manifest.json ├── readmeMedia ├── allowExternal.png ├── buttonPanel_1.png ├── buttonPanel_2.png ├── console.png ├── demo.gif ├── devMode.png ├── importFullLibrary.png ├── paypal.svg ├── shortcutFiles.png └── shortcuts.png ├── src ├── AutoAsyncWrapper.ts ├── AutoComplete.ts ├── Dfc.ts ├── ExternalRunner.ts ├── HelperFncs.ts ├── LibraryImporter.ts ├── ShortcutExpander.ts ├── ShortcutLinks.ts ├── ShortcutLoader.ts ├── _.d.ts ├── _Plugin.ts ├── defaultSettings.ts ├── esbuild.config.mjs ├── package-lock.json ├── package.json ├── tsconfig.json ├── ui_ButtonView.ts ├── ui_InputBlocker.ts ├── ui_Popups.ts ├── ui_dragReorder.ts ├── ui_setting_actions.ts ├── ui_setting_alerts.ts ├── ui_setting_expansionFormat.ts ├── ui_setting_helper.ts ├── ui_setting_other.ts ├── ui_setting_shortcutFiles.ts ├── ui_setting_shortcutFormat.ts ├── ui_setting_shortcuts.ts ├── ui_settings.ts └── ui_userNotifier.ts ├── styles.css ├── tests └── test_preRelease.txt └── todo.md /.gitignore: -------------------------------------------------------------------------------- 1 | data.json 2 | state_auto.data.txt 3 | state_onquit.data.txt 4 | src/node_modules 5 | main.js 6 | *.bak -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Jonathan Heard 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /changelist.md: -------------------------------------------------------------------------------- 1 | ### 0.24.12 2 | - Plugin updates: 3 | - bug fix - library version update alert can show up even if it shouldn't 4 | - feature - shortcut links - remove markdown parsing for shortcut link output (user can do it themselves if necessary) 5 | 6 | ### 0.24.11 7 | - Plugin updates 8 | - feature - helper functions - 3 helper functions added: asyncFilter, asyncMap & asyncForEach. 9 | - Library updates 10 | - feature - tablefiles - can change multiple table configurations simultaneously by selecting multiple tables with shift or ctrl 11 | - polish - tablefiles - popup ui layout & styling polished 12 | - refactor - lists_ui - helper function asyncFilter used instead of internal asyncFilter function. 13 | 14 | ### 0.24.10 15 | - Plugin updates 16 | - feature - settings - Add alerts when there are updates for the plugin or library 17 | - feature - helper function - fileWrite (does "fileCreate" or "fileModify" as appropriate) 18 | - polish - settings - button-view opening button is disable when button-view is opened 19 | - polish - settings - moved "reset settings" button to "Actions" section 20 | - polish - settings - removed separators between common expansion format settings 21 | - Library updates 22 | - feature - tablefiles - added an output format: "array", which returns a raw array. Useful for subshortcutting "tbl roll" 23 | - polish - tablefiles - adjusted frontmatter configurations to be pulled from the cache (speeds up "tbl roll" popup). 24 | - feature - readme - added version number 25 | - bug fix - tablefiles - "tbl roll" (non-ui) checks the path against config tables, but not fm-config tables 26 | - bug fix - tablefiles - table with no configuration is messed up when changing values the first time 27 | 28 | ### 0.24.9 29 | - Plugin updates 30 | - bug fix - autocomplete - not working when shortcut prefix contains shortcut suffix. 31 | 32 | ### 0.24.8 33 | - Plugin updates 34 | - feature - readme - properly documented the useful "unblock()" help function 35 | - feature - readme - properly documented an overview of all help functions 36 | - feature - helperfncs - added helper functions that were around, but not bundled ("print", "runExternal", "popups", "getSettings", "registerView") 37 | - Library updates 38 | - feature - tablefiles - table files can contain a frontmatter YAML with their configuration. 39 | - feature - added confirmations to all "reset" shortcuts in the library. Double-confirmation for "state". 40 | 41 | ### 0.24.7 42 | - Plugin updates 43 | - bug fix - autocomplete - selecting from the list doesn't properly add the selection 44 | - polish - readme - improved the documentation on "common format expansion" 45 | - Library updates 46 | - bug fix - adventurecrafter - "themes fill" is working, but it's output is broken 47 | 48 | ### 0.24.6 49 | - Plugin updates 50 | - bug fix - If an expansion error throws as null, which SOMEHOW actually happened, it's not being handled well. 51 | - Library updates 52 | - bug fix - lists - fixed bug with item renaming not recognizing a combo-list variable 53 | 54 | ### 0.24.5 55 | - Plugin updates 56 | - feature - autocomplete - now shows while typing the shortcut suffix. This is useful as some shortcut-texts use ":". 57 | - Library updates 58 | - feature - tablefiles - registered folders of tables are now pulled recursively. i.e. If a table is in a subfolder then it will also be pulled. 59 | - polish - tablefiles - "tbl roll" shortcut (non-ui version) now accepts registered table titles and basenames case-insensitively 60 | 61 | ### 0.24.4 62 | - Plugin updates 63 | - polish - "ref all" shortcut removes all horizontal rules in descriptions 64 | - feature - autocomplete - special types are handled specially: 65 | - text type highlights indefinitely 66 | - some of the extra-special types have been converted to text "text (details)" 67 | - number types (>0, >=0) only accept numeric values 68 | - path text type handles quotes properly (mostly) 69 | - Library updates 70 | - feature - tablefiles - "tbl roll" shortcut (ui-less one) can take the title or basepath of a registered table in addition to a path to a table file. 71 | - feature - cards - draw and pick now have a variation that allows entering params in any order (using "from " & "to " prefixes) 72 | - polish - cards - removed parameter types. Lets them work with new type checking, and can just use a really big number for "all". 73 | - polish - updated all syntaxes to better adhere to the newly checked parameter types 74 | 75 | ### 0.24.3 76 | - Plugin updates 77 | - feature - ShortcutExpander - added event listening for expansion errors 78 | - Library updates 79 | - added shortcut-file "system" - "sys lasterror", "sys runjs", "sys triggererror" 80 | - polish - une_ui - improved ui. All shortcuts show one big popup instead of multiple little ones 81 | - feature - tablefiles - add shortcut "tbl reroll" 82 | - feature - tablefiles - "tbl reroll" (non-ui version) made parameters case-insensitive 83 | - feature - tablefiles - "tbl reroll" (non-ui version) made format parameter flexible 84 | - removed the need for quotes 85 | - allowed singular value: "comma" instead of commas" 86 | - allowed untrimmed value 87 | - polish - tablefiles - tbl roll (non-ui version) adjusted to be more useful when called from another shortcut 88 | - feature - tablefiles - "tbl roll" shortcut (non-ui version) now accepts a path with implicit ".md" extension 89 | - bug fix - tablefiles - tbl roll (non-ui version) errored each time 90 | 91 | ### 0.24.2 92 | - Plugin updates 93 | - bug fix - shortcutlinks - link doesn't work if part 2 (output script) is included but not defined 94 | 95 | ### 0.24.1 96 | - Plugin updates 97 | - bug fix - append & prepend shortcut links without block-ids are erroring out 98 | - bug fix - rare race condition causes duplicate buttons in buttons view 99 | - Library updates 100 | - bug fix - cards - card images don't show when they have spaces or parenthesis 101 | - bug fix - lists - combo-lists fail 102 | 103 | ### 0.24.0 104 | Plugin updates 105 | - "expFormat()" adds standard expansion formatting to a string or string array. This format is based on settings added under "Expansion format": prefix, line-prefix and suffix. - "expUnformat()" removes standard expansion formatting from a string or string array. 106 | - Shortcut links - can include a block-id to define where the output should go 107 | - Shortcut links - can include a bit of javascript to modify expansion output 108 | - settings - both the autocomplete feature and the autocomplete help tooltip are togglable in the settings 109 | - popups.pick - allow turning the dropdown into a list box of either a fixed size, or adaptive size to it's contents 110 | - popups.custom - "resolveFnc()" passed to onOpen to allow early outing with custom result 111 | - helper function - "appendToEndOfNote()" replaced with more versatile "addToNote()" function 112 | - helper function - "unblock()" added to manually ublock if the input blocker is left on, such as if a shortcut errors out. 113 | - helper function - "getLeafForFile()" is replaced with "getLeavesForFile()" 114 | - bug fix - the latest Obsidian release breaks some ui in the inline scripts buttons view 115 | Library updates 116 | - tablefiles - shortcut-file added! Allows working with files of tables in different formats. 117 | - cards 2.0 - a new interface for working with virtual cards. It has a number of incompatibilities with the older interface. 118 | - state - state file now saved to same folder as the state shortcut-file. This ensures the state is properly transferred with the rest of the vault, regardless of what happens to the .obsidian folder. 119 | - state - state saving is now done after each shortcut is run, rather than on a timer 120 | - notevars - resolved incompatibilities with latest Obsidian 121 | - notevars - resolved bug where running "notevars set" multiple times only saved one of the changes 122 | - general - commented entire library 123 | - general - shortcuts that use pick popups now show lists instead of dropdowns. 124 | - files - shortcut-file added! Shortcuts for working with files, including: "extensionChange" and "files shortcutbatch" 125 | - lists - added "lists rename" and "lists shortcutbatch" 126 | 127 | ### 0.23.2 128 | - bug fix - state is occasionally not saved on Obsidian quit. 129 | 130 | ### 0.23.1 131 | - bug fix - fixed announcement to include "import latest library" message. 132 | 133 | ### 0.23.0 134 | LIBRARY UPDATES 135 | - The state system now keeps the state between Obsidian sessions without needing user interaction. 136 | - Shortcut added - "lists fromfile lines {list name} {file name}" reads a file and adds each of its lines to a list as an item. 137 | - Cards system updates: 138 | - Can modify the cards through the card-pile viewer panel. Drag to reorder and double-click to rotate. 139 | - Custom card-backing. 140 | - Global card size. 141 | - Can predefine card-piles with the shortcut "cards pile {pile id}" 142 | - Square cards now rotate to any of four directions. Non-square cards still only rotate to two directions: rightside-up and upside-down. 143 | PLUGIN 144 | - Shortcuts are now case-insensitive (useful for mobile, which auto-cases letters) 145 | - "getObsidianInterfaces" was merged into "helperFncs" 146 | - "onShortcutsLoaded" event added. 147 | - Shutdown scripts are called when closing Obsidian. 148 | - The drag-reorder system is improved. 149 | - Button panel's create/edit button popup has better field descriptions. 150 | BUG-FIXES 151 | - Card images in notes are absolute, so break when vault is moved or synced. 152 | - Drag-reorder not working on mobile. 153 | ### 0.22.2 154 | LIBRARY UPDATES 155 | - Interfacing with the Dice Roller plugin made possible with the new "plugin_diceroller" shortcut-file. 156 | - Contains a single shortcut ;;diceroller {command}:: 157 | - Virtual cards within Obsidian made possible with the new "cards" shortcut-file. 158 | - Check out the tutorial video: https://www.youtube.com/watch?v=-m4n7d3aKC8 159 | - A new panel type to view card-piles added with the new "cards_pileviewer" shortcut-file. 160 | - More user-friendly versions of shortcuts added with the "cards_ui" shortcut-file. 161 | - notepick now uses the state system to save state between Obsidian sessions. 162 | FEATURES 163 | - Functions "addCss()" and "removeCss()" provided to allow shortcut-files to use their own CSS. Added to "window._inlineScripts.inlineScripts.helperFncs". 164 | - Can now customize button texts for popup panels. 165 | - popups.input now takes an optional "suggestions" array that will show in a dropdown list beneath the textbox (if provided). 166 | - Plugin access added at "window._inlineScripts.inlineScripts.plugin" 167 | - Plugin has getObsidianInterfaces() function that returns some useful Obsidian interfaces. 168 | BUG-FIXES 169 | - Current parameter isn't highlighted if space-skipping past prior optional ones. 170 | - "window._inlineScripts" is'n't removed when plugin is disabled 171 | 172 | ### 0.22.1 173 | - bug fix - announcement - latest has a broken link. 174 | - polish - announcement - added bullet to announce support shortcut-files. 175 | 176 | ### 0.22.0 177 | FEATURES 178 | - A button panel has been added to plugin, allowing custom button creation for running shortcuts 179 | - Button panel buttons can run shortcuts with "???" run-time parameters 180 | - Button panel groups allow setting up groups of buttons for different tasks 181 | - Shortcut-files automatically get their own button panel groups 182 | - Shortcut links 183 | - Links that will trigger a shortcut on clicked, either once or on each click. 184 | - Can define run-time parameter captions in the link 185 | - In the library, many shortcut-files now have "x_ui" suppliment shortcut-files to redo complex shortcuts in a more graphical ui way. 186 | - Global variable setup in shortcut-files has been streamlined with the addition of "confirmObjectPath()" 187 | - Added video tutorials for a number of shortcut-files in the library. 188 | - Added a variable with list of the loaded shortcut-files and their registered order. 189 | - Made a collection of helper functions available to shortcuts 190 | BREAKING CHANGES 191 | - About string syntax - parameters no longer include "required" or "optional". 192 | - "Optional" parameters are now any parameters that specify a default value. 193 | - The variable where session state is stored has changed location: 194 | - It has moved from "_inlineScripts.state" to "_inlineScripts.state.sessionState" 195 | - In the library, the new "x_ui" suppliment shortcut-files redo some of the more complex shortcuts. It's possible that this may cause some confusion, if the user isn't aware of them. 196 | NON-BREAKING CHANGES 197 | - Startup and shutdown scripts can now use helper scripts 198 | - Warning added when shortcut-file is registered, but has no shortcuts 199 | - Can display multiple version annoucements when plugin is updated 200 | - Buymecoffee donation method added. Donation methods added to bottom of settings. 201 | 202 | ### 0.21.5 203 | - bug fix - ios platform incompatible with regex look-behinds. Replace all lookbehinds! 204 | 205 | ### 0.21.4 206 | - bug fix - disabling plugin causes "hidden" autocomplete description div to show 207 | 208 | ### 0.21.3 209 | - polish - made announcement message applicable to all 0.21.x 210 | 211 | ### 0.21.2 212 | - bug fix - announcement doesn't show properly in all situations 213 | 214 | ### 0.21.1 215 | - feature - default settings now include the plugin's current version 216 | - polish - release announcement - Refer to plugin as "Inline Scripts" 217 | 218 | ### 0.21.0 219 | - feature - shortcut autocomplete with shortcut descriptions 220 | - refactor - Plugin rename: "Text Expander JS" to "Inline Scripts" 221 | - feature - default prefix & suffix set to ";;" & "::" for all platforms 222 | - feature - sfile sections dividied by "__" instead of "~~" 223 | - feature - sfiles now use naming convention: "***.sfile.md" 224 | 225 | ### 0.20.1 226 | - bug fix - format settings - added block against prefix & suffix containing characters with auto-complete 227 | 228 | ### 0.20.0 229 | - feature - "expansionInfo" expansion parameter added, along with "cancel" settable member to cancel the expansion. 230 | - feature - settings now have a "Reset to defaults" button. 231 | - feature - library importer - now lets user pick the vault path for the imported library. 232 | - bug fix - backslashes in the shortcut-text are removed when expanded. 233 | - bug fix - library files are cached during library import. If they are changed, during execution, repimport won't include changes. If internet is lost, reimport won't fail. 234 | 235 | ### 0.19.1 236 | - feature - Allow library to determine the disabled flags for importing shortcut-files. 237 | 238 | ### 0.19.0 239 | - feature - Added a ui to disable shortcut-files without removing them from the shortcut-file settings list 240 | - feature - help system updated. General "help" is available to setup scripts and can be checked for loaded sfiles. 241 | - feature - added a "failSilently" parameter to the expand() function 242 | 243 | ### 0.18.0 244 | - bug fix - Import Library feature doesn't fail gracefully on unable to connect to repo. 245 | - feature - Shortcut-files can be disabled, without removing them. Feature implemented, but not yet exposed through UI. 246 | 247 | ### 0.17.2 248 | - bug fix - shortcut-files with non-standard newlines (\r chars) cause bugs in the help system. 249 | 250 | ### 0.17.1 251 | - polish - only run require("child_process") if on non-mobile platform. 252 | - polish - "==" and "!=" to "===" and "!==" 253 | - bug fix - Renaming the active note makes "_currentFilesName" inaccurate. If the active note is a shortcut-file, this causes a minor bug until the active note is changed. 254 | 255 | ### 0.17.0 256 | - feature - Each shortcut now has an "About" string, to store a short documentation on the shortcut. 257 | - feature - A robust help system is now available, built around the shortcut "About" string. 258 | - feature - shortcut-files are now parsed properly when they have a metadata frontmatter section. 259 | - feature - Dfc now ALWAYS refreshes shortcuts when shortcut-file is modified. Dev-mode causes refresh on sfile touch. 260 | - feature - shortcut-files can now contain shutdown scripts: shortcuts run when shortcut-file is removed, or plugin is disabled. 261 | - feature - A new function is available to shortcuts: print(message) shows message in popup and console, then returns the message. 262 | - feature - If a setup script returns true, the shortcut-file's shortcuts are not loaded into the system. 263 | - feature - Feature to allow adding callbacks for when an expansion occurs 264 | - feature - "Import Library" now always maintains the order of library shortcut-files when imported into the shortcut-files list. 265 | - refactor - the "getExpansion()" function, runnable from shortcuts, is now "expand()". 266 | 267 | ### 0.16.14 268 | - polish - added pre-release test: a text file with steps to test ALL features of TEJS. 269 | 270 | ### 0.16.13 271 | - bug fix - if expansion returns something other than string or string array, it's not handled right. 272 | 273 | ### 0.16.12 274 | - Polish - Wrote DEFAULT_SETTING.shortcuts with backtick string for readability 275 | - Polish - fixed typo 276 | - Polish - changed "greet" shortcut to "hi" 277 | - Polish - Put Object.freeze into DEFAULT_SETTINGS assignment 278 | 279 | ### 0.16.11 280 | - Polished code - refactored "settings.getShortcutReferencesFromUi()" to "settings.getShortcutFilesFromUi()" 281 | - Polished code - Improved code readability of DEFAULT_SETTINGS 282 | 283 | ### 0.16.10 284 | - review response - Replaced "plugin.app.isMobile" with "obsidian.Platform.isMobile". 285 | 286 | ### 0.16.9 287 | - bug fix - changing the shortcut-files list in the settings ui, then immediately importing the library causes the changes to be reverted. 288 | 289 | ### 0.16.8 290 | - Responded to review: replaced plugin's platform vars with direct API references. 291 | - Responded to review: renamed local copy of "activeFile" to better signify its meaning (a cache to reference upon the activeFile changing). 292 | - Improved comments in Dfc class. 293 | 294 | ### 0.16.7 295 | - Polish code - minor fixes for review 296 | 297 | ### 0.16.6 298 | - Polish code - minor fixes for review 299 | 300 | ### 0.16.5 301 | - Polish code - minor fixes for review 302 | 303 | ### 0.16.4 304 | - All "classes" are now official JS classes. 305 | 306 | ### 0.16.3 307 | - bug fix: app was glitching out at start due to not waiting for system to be ready before pulling files 308 | 309 | ### 0.16.2 310 | - Code polish (for passing review quickly) 311 | 312 | ### 0.16.1 313 | - Code polish (for passing review quickly) 314 | 315 | ### 0.16.0 316 | - Add defaults - doesn't allow duplicating default shortcuts that are already in the list. Removes preexisting defaults from list, then append all defaults to list. 317 | - __Text Expander JS__ plugin version shows at top and bottom of settings 318 | 319 | ### 0.15.3 320 | - bug fix: Input-block was blocking a user choice that was triggered after it was added 321 | 322 | ### 0.15.2 323 | - bug fix: Added input block for while importing library (since async file downloading does not) 324 | - bug fix: import library function asks user for choice as if "tejs" library folder is different from "tejs". 325 | 326 | ### 0.15.1 327 | - bug fix: "help" system doesn't recognize help shortcuts that include numbers or underscore 328 | 329 | ### 0.15.0 330 | - new feature: "Import full library" button in settings - downloads and sets up the entire Text Expander JS shortcut-file library into the vault 331 | - bug fix: when on mobile, settings with multiple buttons don't separate buttons enough 332 | - bug fix: bad shortcut-file references aren't red on opening settings 333 | 334 | ### 0.14.3 335 | - bug fix: console error for each ; typed that doesn't expand 336 | 337 | ### 0.14.2 338 | - Merged CM5 and CM6 expansion code. 339 | 340 | ### 0.14.1 341 | - bug fix: error with CM5 (old editor) and shortcuts that return string arrays. 342 | 343 | ### 0.14.0 344 | - Added up/down buttons for shortcut-files and shortcuts lists in settings 345 | 346 | ### 0.13.2 347 | - bug fix: error during expansion can cause out-of-date editor issues. 348 | 349 | ### 0.13.1 350 | - bug fix: shortcuts without return statements have their expansion script run properly, but still trigger "shortcut unidentified". 351 | 352 | ### 0.13.0 353 | - Add ability to run external applications through the "runExternal" function (not available through mobile). 354 | - bug fix: erroring on a shortcut _after_ it has called getExpansion produces an "uncaught" error, rather than the proper, useful error. 355 | 356 | ### 0.12.1 357 | - bug fix: minor: settings ui: format example misaligned 358 | 359 | ### 0.12.0 360 | - Empty shortcut is "helperblock": it clears out helper scripts. It is auto-added to the end of each shortcut-file 361 | - add an automatic "help" shortcut that lists all "* help" lines. 362 | - add to default shortcuts: date, time, datetime 363 | - Replaced MyPlugin and MySettings titles 364 | - Removed expansion trigger options (now only expands on final key hit) 365 | - shortcut tests are now stored as regexp objects, instead of strings 366 | - All CSS classes now prefixed with "tejs_" to avoid overlap with other plugins 367 | - Expansions strings can now be surrounded with a JavaScript fenced code block. Test strings can be surrounded with a basic fenced code block. 368 | - Expansion scripts can now return an array of strings. This allows segmentation of the data, though the string array is joined during expansion. 369 | - Expansion scripts now have access to "getExpansion(text)" to allow calling other shortcuts and using their results. 370 | 371 | ### 0.11.0 372 | - Decent error messaging for parsing shortcut-files and when shortcut isn't recognized 373 | - change shortcut from json to custom format: "~~" 374 | - create scripts to playtest 375 | - Fill in rest of readme instructions 376 | - confirm plugin works on iphone 377 | - polish code 378 | 379 | ### 0.10.0 380 | - Remove "expansion trigger" option for mobile 381 | - **Settings**: Developer mode: monitor shortcut-files for changes 382 | - polish settings ui on mobile 383 | - Default settings different on mobile vs non-mobile (prefix/suffix) 384 | - bug fix: expansion incorrect with non-1-sized suffix 385 | - fix bug: changing prefix/suffix requires plugin reload 386 | 387 | ### 0.9.0 388 | - **Settings**: Shortcuts (definable directly in settings) 389 | - **Settings**: each shortcut-file should have a delete button (no global "Remove file" button) 390 | - Get working on mobile 391 | 392 | ### 0.8.0 393 | - **Settings**: Custom CSS filename 394 | - Replace "alert" with alternative that doesn't mess up caret 395 | - CSS file added for settings UI (replaces inline styles) 396 | 397 | ### 0.7.0 398 | - Adjust version format (final digit has 3 spaces, not 4) 399 | - Fix ";;"/";" bookends to work when caret is on prefix 400 | - **Settings**: Shortcut prefix & postfix 401 | - **Settings**: Shortcut definitions filename 402 | - **Settings**: Shortcut expansion hotkey 403 | 404 | ### 0.6.0 405 | - Allow building a result from multiple shortcuts (to allow common code) 406 | - Allow replacer to be either a string, or an array of strings to be concatenated together 407 | - Console log when loading/unloading plugin 408 | - Have version follow format convention (##.##.####) 409 | 410 | ### 0.5.0 411 | - Basic implementation. All settings hardwired 412 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-text-expander-js", 3 | "name": "Inline Scripts", 4 | "version": "0.24.12", 5 | "minAppVersion": "0.12.0", 6 | "description": "Type text shortcuts which are then replaced with JavaScript generated text.", 7 | "author": "Jonathan Heard", 8 | "authorUrl": "https://github.com/jon-heard", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /readmeMedia/allowExternal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jon-heard/obsidian-inline-scripts/acd25b6009232ac15f4df22582d127bf165e417b/readmeMedia/allowExternal.png -------------------------------------------------------------------------------- /readmeMedia/buttonPanel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jon-heard/obsidian-inline-scripts/acd25b6009232ac15f4df22582d127bf165e417b/readmeMedia/buttonPanel_1.png -------------------------------------------------------------------------------- /readmeMedia/buttonPanel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jon-heard/obsidian-inline-scripts/acd25b6009232ac15f4df22582d127bf165e417b/readmeMedia/buttonPanel_2.png -------------------------------------------------------------------------------- /readmeMedia/console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jon-heard/obsidian-inline-scripts/acd25b6009232ac15f4df22582d127bf165e417b/readmeMedia/console.png -------------------------------------------------------------------------------- /readmeMedia/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jon-heard/obsidian-inline-scripts/acd25b6009232ac15f4df22582d127bf165e417b/readmeMedia/demo.gif -------------------------------------------------------------------------------- /readmeMedia/devMode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jon-heard/obsidian-inline-scripts/acd25b6009232ac15f4df22582d127bf165e417b/readmeMedia/devMode.png -------------------------------------------------------------------------------- /readmeMedia/importFullLibrary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jon-heard/obsidian-inline-scripts/acd25b6009232ac15f4df22582d127bf165e417b/readmeMedia/importFullLibrary.png -------------------------------------------------------------------------------- /readmeMedia/paypal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /readmeMedia/shortcutFiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jon-heard/obsidian-inline-scripts/acd25b6009232ac15f4df22582d127bf165e417b/readmeMedia/shortcutFiles.png -------------------------------------------------------------------------------- /readmeMedia/shortcuts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jon-heard/obsidian-inline-scripts/acd25b6009232ac15f4df22582d127bf165e417b/readmeMedia/shortcuts.png -------------------------------------------------------------------------------- /src/AutoAsyncWrapper.ts: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // AutoAsyncWrapper - Takes a string of JS source and wraps certain functions in "await". // 3 | // Simplifies scripts by letting them not explicitly require "await". // 4 | //////////////////////////////////////////////////////////////////////////////////////////////////// 5 | 6 | "use strict"; 7 | 8 | const REGEX_AWAIT_TEMPLATE: string = "(?:~1~)[\s]*\\("; 9 | const REGEX_ASYNC: RegExp = /function[\s]*\(/g; 10 | const UNNESTABLE_BLOCK_PAIRS: any = Object.freeze( 11 | { 12 | "\"": "\"", 13 | "'": "'", 14 | "`": "`", 15 | }); 16 | const NESTABLE_BLOCK_PAIRS: any = Object.freeze( 17 | { 18 | "(": ")", 19 | "[": "]", 20 | "{": "}", 21 | }); 22 | 23 | export namespace AutoAsyncWrapper 24 | { 25 | export function initialize(toAwaitWrap: Array) 26 | { 27 | _regex_await = new RegExp(REGEX_AWAIT_TEMPLATE.replace("~1~", toAwaitWrap.join("|")), "g"); 28 | } 29 | 30 | // Pull the official TEJS library from github & add it to the current vault 31 | export function run(source: string): string 32 | { 33 | return run_internal(source); 34 | } 35 | 36 | /////////////////////////////////////////////////////////////////////////////////////////////////// 37 | 38 | let _regex_await: RegExp; 39 | 40 | function run_internal(source: string): string 41 | { 42 | source = addPrefixToAllInstances(source, "async ", REGEX_ASYNC); 43 | source = wrapPrefixToAllInstances(source, "await ", _regex_await); 44 | return source; 45 | } 46 | 47 | function addPrefixToAllInstances( 48 | source: string, prefix: string, regex_searchToWrap: RegExp): string 49 | { 50 | let matchPositions: Array = []; 51 | 52 | for (const match of source.matchAll(regex_searchToWrap)) 53 | { 54 | matchPositions.push({ start: match.index, end: match.index + match[0].length }); 55 | } 56 | 57 | matchPositions.reverse(); 58 | 59 | for (const matchPosition of matchPositions) 60 | { 61 | source = 62 | source.slice(0, matchPosition.start) + prefix + 63 | source.slice(matchPosition.start); 64 | } 65 | 66 | return source; 67 | } 68 | 69 | function wrapPrefixToAllInstances( 70 | source: string, prefix: string, regex_searchToWrap: RegExp): string 71 | { 72 | let matchPositions: Array = []; 73 | 74 | for (const match of source.matchAll(regex_searchToWrap)) 75 | { 76 | matchPositions.push({ start: match.index, end: match.index + match[0].length }); 77 | } 78 | 79 | matchPositions.reverse(); 80 | 81 | for (const matchPosition of matchPositions) 82 | { 83 | const stack: Array = [ ")" ]; 84 | let index: number = matchPosition.end; 85 | const sLength: number = source.length; 86 | let isNestable: boolean = true; 87 | while (index < sLength) 88 | { 89 | if (source[index] === stack[stack.length-1] && (source[index-1] != "\\")) 90 | { 91 | stack.pop(); 92 | isNestable = true; 93 | if (!stack.length) 94 | { 95 | break; 96 | } 97 | } 98 | else if (isNestable) 99 | { 100 | let pairing = UNNESTABLE_BLOCK_PAIRS[source[index]]; 101 | if (pairing) 102 | { 103 | stack.push(pairing); 104 | isNestable = false; 105 | } 106 | else 107 | { 108 | pairing = NESTABLE_BLOCK_PAIRS[source[index]]; 109 | if (pairing) 110 | { 111 | stack.push(pairing); 112 | } 113 | } 114 | } 115 | index++; 116 | } 117 | 118 | source = 119 | source.slice(0, matchPosition.start) + "(" + prefix + 120 | source.slice(matchPosition.start, index) + ")" + source.slice(index); 121 | } 122 | 123 | return source; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/AutoComplete.ts: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////// 2 | // AutoComplete - Show an autocomplete ui while typing a shortcut // 3 | //////////////////////////////////////////////////////////////////// 4 | 5 | "use strict"; 6 | 7 | import { EditorSuggest } from "obsidian"; 8 | import InlineScriptsPlugin from "./_Plugin"; 9 | import { HelperFncs } from "./HelperFncs"; 10 | 11 | const SUGGESTION_LIMIT = 1000; 12 | 13 | const REGEX_SYNTAX_SPLITTER: RegExp = /~~}|(?=\{)/; 14 | const REGEX_FIRST_PARAMETER_START: RegExp = / ?\{/; 15 | 16 | export class AutoComplete extends EditorSuggest 17 | { 18 | public constructor(plugin: InlineScriptsPlugin) 19 | { 20 | super(plugin.app); 21 | this.constructor_internal(plugin); 22 | } 23 | 24 | public destructor() 25 | { 26 | document.getElementById("shortcutSuggestionDescription")?.remove(); 27 | } 28 | 29 | public onTrigger(cursor: any, editor: any): any 30 | { 31 | return this.onTrigger_internal(cursor, editor); 32 | } 33 | 34 | public getSuggestions(context: any): any 35 | { 36 | return this.getSuggestions_internal(context); 37 | }; 38 | 39 | public renderSuggestion(suggestion: any, el: any): void 40 | { 41 | this.renderSuggestion_internal(suggestion, el); 42 | }; 43 | 44 | selectSuggestion(suggestion: any): void 45 | { 46 | this.selectSuggestion_internal(suggestion); 47 | }; 48 | 49 | /////////////////////////////////////////////////////////////////////////////////////////////////// 50 | 51 | // Keep the plugin for a few different uses 52 | private _plugin: InlineScriptsPlugin; 53 | // Keep a bound version of this method to pass into a sort method 54 | private _resortSyntaxes: any; 55 | // The original function called on a modification to an internal function 56 | private _forceSetSelectedItem: Function; 57 | // The original function called on showing the suggestion list ui 58 | private _open: Function; 59 | // The original function called on hiding the suggestion list ui 60 | private _close: Function; 61 | // A UI panel created to display the description of the currently suggested shortcut 62 | private _suggestionDescriptionUi: any; 63 | // A list of the descriptions for the currently suggested shortcuts 64 | private _descriptions: Array; 65 | 66 | // Members of EditorSuggest not included in it's type definition 67 | private suggestions: any; 68 | private suggestEl: any; 69 | 70 | private constructor_internal(plugin: InlineScriptsPlugin) 71 | { 72 | // Plugin stored for a few different uses 73 | this._plugin = plugin; 74 | 75 | this.limit = SUGGESTION_LIMIT; 76 | 77 | // Keep a bound version of this method to pass into a sort method 78 | this._resortSyntaxes = this.resortSyntaxes.bind(this); 79 | 80 | // Modify original functions - forceSelectedItem, open, close 81 | this._forceSetSelectedItem = this.suggestions.forceSetSelectedItem.bind(this.suggestions); 82 | this.suggestions.forceSetSelectedItem = this.forceSelectedItem_modified.bind(this); 83 | this._open = this.open; 84 | this.open = this.open_modified; 85 | this._close = this.close; 86 | this.close = this.close_modified; 87 | 88 | // Get or create a new UI panel to show the description of the currently suggested shortcut 89 | this._suggestionDescriptionUi = document.getElementById("shortcutSuggestionDescription"); 90 | if (!this._suggestionDescriptionUi) 91 | { 92 | plugin.app.workspace.onLayoutReady(() => 93 | { 94 | this._suggestionDescriptionUi = document.createElement("div"); 95 | this._suggestionDescriptionUi.id = "shortcutSuggestionDescription"; 96 | this._suggestionDescriptionUi.classList.add("iscript_suggestionDescription"); 97 | 98 | const parent = document.querySelector(".workspace-split.mod-root"); 99 | parent.insertBefore(this._suggestionDescriptionUi, parent.firstChild); 100 | }); 101 | } 102 | } 103 | 104 | // Called by the system to determine if auto-complete should pop up at all 105 | private onTrigger_internal(cursor: any, editor: any): any 106 | { 107 | // If autocomplete is turned off, early out (obviously) 108 | if (InlineScriptsPlugin.getInstance().settings.autocomplete === false) 109 | { 110 | return; 111 | } 112 | 113 | // Get the shortcut prefix and suffix 114 | const prefix: string = this._plugin.settings.prefix; 115 | const suffix: string = this._plugin.settings.suffix; 116 | 117 | // Get the current line of text up to the caret position 118 | const lineUpToCursor: string = editor.getLine(cursor.line).slice(0, cursor.ch); 119 | 120 | // Look for whether we are within a shortcut (after a prefix, and NOT after a suffix) 121 | let shortcutUnderCaret = null; 122 | let shortcutStart = lineUpToCursor.lastIndexOf(prefix); 123 | if (shortcutStart !== -1) 124 | { 125 | if (lineUpToCursor.indexOf(suffix, shortcutStart + prefix.length) === -1) 126 | { 127 | shortcutUnderCaret = lineUpToCursor.slice(shortcutStart + prefix.length); 128 | } 129 | } 130 | 131 | // If we ARE within a shortcut, auto-complete should pop up 132 | if (shortcutUnderCaret !== null) 133 | { 134 | return { 135 | end: cursor, 136 | start: 137 | { 138 | ch: lineUpToCursor.length - shortcutUnderCaret.length, 139 | line: cursor.line, 140 | }, 141 | query: shortcutUnderCaret, 142 | }; 143 | } 144 | else 145 | { 146 | return null; 147 | } 148 | } 149 | 150 | // Called by the system to get a list of suggestions to display in auto-complete 151 | private getSuggestions_internal(context: any): any 152 | { 153 | // Early out if syntaxesSorted isn't available 154 | if (!this._plugin?.syntaxesSorted) { return null; } 155 | 156 | // Get ALL shortcut syntaxes 157 | const result = this._plugin.syntaxesSorted 158 | // Check each syntax against the query (i.e. the user's current shortcut-text) 159 | .map((p: any) => 160 | { 161 | p.match = context.query.match(p.regex); 162 | return p; 163 | }) 164 | // Filter syntaxes down to those that match the query 165 | .filter((p: any) => 166 | { 167 | return p.match; 168 | }) 169 | // Sort syntaxes by how much they match the query 170 | .sort(this._resortSyntaxes); 171 | // Fill the descriptions list with descriptions of the listed syntaxes 172 | this._descriptions = result.map((v: any) => v.description); 173 | return result; 174 | }; 175 | 176 | // Called by the system to determine HOW to render a given suggestion 177 | private renderSuggestion_internal(suggestion: any, el: any): void 178 | { 179 | // Get the normal suggestion text 180 | let text = suggestion.text.replace("<", "<"); 181 | 182 | // If the suggestion contains parameters, try and modify the suggestion text to highlight 183 | // the parameter the user is currently on. 184 | if (suggestion.match.length > 1) 185 | { 186 | let currentParameterIndex = -1; 187 | // TODO - Uncomment this after fixing the TODO below 188 | // let inNonParameterSection = false; 189 | 190 | // Artificially add a dummy character to see if it's accepted. If so, we're in a 191 | // parameter section, since they accept ANY character, except space. We use the 192 | // "unit separator" for the dummy character as it's unlikely to show false positive. 193 | let match = (this.context.query + "\u241F").match(suggestion.regex); 194 | if (match) 195 | { 196 | // Find which parameter section accepted the dummy character. 197 | for (let i = 1; i < match.length; i++) 198 | { 199 | if (match[i].endsWith("\u241F")) 200 | { 201 | currentParameterIndex = i; 202 | break; 203 | } 204 | } 205 | } 206 | // The dummy character was NOT accepted, so we're in a hardcoded section. Determine 207 | // the last parameter section that was accepted and we're in the section just beyond 208 | // that one. If NO parameter sections were accepted, we're in the hardcoded section 209 | // before ANY parameter sections, so don't set currentParameterIndex and NO sections 210 | // will be highlighted. 211 | else 212 | { 213 | // TODO - Fix this so that it works if skipping an optional parameter section into 214 | // a hardcoded one. 215 | 216 | // inNonParameterSection = true; 217 | // match = suggestion.match; 218 | // // Find which parameter section accepted the dummy character. 219 | // for (let i = 1; i < match.length; i++) 220 | // { 221 | // if (match[i]) 222 | // { 223 | // currentParameterIndex = i; 224 | // } 225 | // } 226 | } 227 | 228 | if (currentParameterIndex !== -1) 229 | { 230 | // Split the suggestion text into parts, including the parameter sections. 231 | const parts = text.replaceAll("}", "}~~}").split(REGEX_SYNTAX_SPLITTER); 232 | // Find and highlight the current section. 233 | let parameterCounter = 0; 234 | for (let i = 0; i < parts.length; i++) 235 | { 236 | if (parts[i].startsWith("{") && parts[i].endsWith("}")) 237 | { 238 | parameterCounter++; 239 | if (parameterCounter == currentParameterIndex) 240 | { 241 | // TODO - Uncomment after fixing TODO above this one 242 | // if (inNonParameterSection) { i++; } 243 | parts[i] = 244 | "" + parts[i] + ""; 245 | text = parts.join(""); 246 | break; 247 | } 248 | } 249 | } 250 | } 251 | } 252 | 253 | el.innerHTML = text; 254 | }; 255 | 256 | // Called by the system when the user selects one of the suggestions 257 | private selectSuggestion_internal(suggestion: any): void 258 | { 259 | // Do nothing if this is called without any context 260 | if (!this.context) { return; } 261 | 262 | // Get the suggestion's "fill": all of the suggestion text before the first parameter 263 | const suggestionEndIndex: number = 264 | suggestion.text.match(REGEX_FIRST_PARAMETER_START)?.index ?? suggestion.text.length; 265 | const fill = suggestion.text.slice(0, suggestionEndIndex); 266 | 267 | // If the current shortcut-text doesn't yet have all the fill, set it to the fill 268 | if (!this.context.query.startsWith(fill)) 269 | { 270 | this.context.editor.replaceRange(fill, this.context.start, this.context.end); 271 | this.context.start.ch += fill.length; 272 | this.context.editor.setCursor(this.context.start); 273 | } 274 | 275 | // The current shortcut-text already has all the fill, and the fill is all there is (no 276 | // parameters). Try expanding the shortcut text. 277 | else if (fill === suggestion.text) 278 | { 279 | const plugin: InlineScriptsPlugin = InlineScriptsPlugin.getInstance(); 280 | this.context.editor.replaceRange( 281 | plugin.settings.suffix, this.context.end, this.context.end); 282 | this.context.end.ch += plugin.settings.suffix.length; 283 | this.context.editor.setCursor(this.context.end); 284 | plugin.tryShortcutExpansion(); 285 | } 286 | 287 | // The current shortcut-text already has all the fill, but there are parameters. Are all 288 | // parameters satisfied? (either filled or with default) If so, try expanding. 289 | else 290 | { 291 | const parts = suggestion.text.replaceAll("}", "}~~}").split(REGEX_SYNTAX_SPLITTER); 292 | let parameterIndex = suggestion.match.length - 1; 293 | for (let i = parts.length - 1; i >= 0; i--) 294 | { 295 | if (parts[i].startsWith("{")) 296 | { 297 | // If parameter isn't fulfilled and has no default, end now 298 | if (!suggestion.match[parameterIndex] && !parts[i].includes("default")) 299 | { 300 | break; 301 | } 302 | parameterIndex--; 303 | } 304 | if (parameterIndex === 0) 305 | { 306 | const plugin: InlineScriptsPlugin = InlineScriptsPlugin.getInstance(); 307 | this.context.editor.replaceRange( 308 | plugin.settings.suffix, this.context.end, this.context.end); 309 | this.context.end.ch += plugin.settings.suffix.length; 310 | this.context.editor.setCursor(this.context.end); 311 | plugin.tryShortcutExpansion(); 312 | break; 313 | } 314 | } 315 | } 316 | }; 317 | 318 | // Used to sort syntaxes by how far they match the query (i.e. the user's current shortcut-text) 319 | private resortSyntaxes(a: any, b: any): number 320 | { 321 | return this.indexOfDifference(b.text, this.context.query) - 322 | this.indexOfDifference(a.text, this.context.query); 323 | } 324 | 325 | // Determine the index of the first non-matching character of two strings 326 | private indexOfDifference(a: string, b: string): number 327 | { 328 | for (let i = 0; i < a.length; i++) 329 | { 330 | if (i >= b.length) 331 | { 332 | return b.length; 333 | } 334 | if (a[i] !== b[i]) 335 | { 336 | return i; 337 | } 338 | } 339 | return a.length; 340 | } 341 | 342 | // The internal function "forceSelectedItem" is modified to do it's usual job AND to update the 343 | // suggestion description UI 344 | private forceSelectedItem_modified(e: any,t: any) 345 | { 346 | this._forceSetSelectedItem(e, t); 347 | this._suggestionDescriptionUi.setText(""); 348 | this._suggestionDescriptionUi.innerHTML = 349 | HelperFncs.parseMarkdown(this._descriptions[this.suggestions.selectedItem]); 350 | } 351 | 352 | private open_modified() 353 | { 354 | this._open(); 355 | 356 | if (InlineScriptsPlugin.getInstance().settings.autocompleteHelp === false) 357 | { 358 | return; 359 | } 360 | this._suggestionDescriptionUi.style.display = "unset"; 361 | // Put descriptionUi at top or bottom of suggestions? 362 | setTimeout(() => 363 | { 364 | const suggestListRect: any = this.suggestEl.getBoundingClientRect(); 365 | const bottom = suggestListRect.y + suggestListRect.height; 366 | if (bottom > window.innerHeight * 0.7) 367 | { 368 | this._suggestionDescriptionUi.classList.add("iscript_suggestionDescription_above"); 369 | } 370 | else 371 | { 372 | this._suggestionDescriptionUi.classList. 373 | remove("iscript_suggestionDescription_above"); 374 | } 375 | }, 0); 376 | } 377 | 378 | private close_modified() 379 | { 380 | this._close(); 381 | this._suggestionDescriptionUi.style.display = "none"; 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /src/Dfc.ts: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////////////////////// 2 | // Dynamic File Content (dfc) - Maintain a list of files to (optionally) monitor for updates // 3 | /////////////////////////////////////////////////////////////////////////////////////////////// 4 | 5 | "use strict"; 6 | 7 | import InlineScriptsPlugin from "./_Plugin"; 8 | 9 | export enum DfcMonitorType { None, OnModify, OnTouch }; 10 | 11 | export class Dfc 12 | { 13 | public constructor( 14 | filenames: Array, refreshFnc: Function, onFileRemoved: Function, 15 | fileOrderImportant: boolean) 16 | { 17 | this.constructor_internal(filenames, refreshFnc, onFileRemoved, fileOrderImportant); 18 | } 19 | 20 | // Called when this Dfc is no longer needed. 21 | public destructor(): void 22 | { 23 | this.setMonitorType(DfcMonitorType.None); 24 | } 25 | 26 | // Define what triggers this Dfc to call the refreshFnc 27 | // - None - refreshFnc is never called 28 | // - OnModify - refreshFnc is called when a monitored file is changed, and moved away from. 29 | // - OnTouch - refreshFnc is called when a monitored file is moved away from. 30 | public setMonitorType(monitorType: DfcMonitorType): void 31 | { 32 | this.setMonitorType_internal(monitorType); 33 | } 34 | 35 | // Pass in a new list of files to monitor. 36 | // The Dfc's current monitored files list is updated to match. 37 | // If this ends up changing the Dfc's list, refreshFnc called. 38 | // Alternately, forceRefresh being true will force refreshFnc to be called. 39 | public updateFileList(newFileList: Array, forceRefresh?: boolean) : void 40 | { 41 | this.updateFileList_internal(newFileList, forceRefresh); 42 | } 43 | 44 | /////////////////////////////////////////////////////////////////////////////////////////////////// 45 | 46 | private _refreshFnc: Function; 47 | private _fileOrderImportant: boolean; 48 | private _onFileRemoved: Function; 49 | private _fileData: any; 50 | private _monitorType: DfcMonitorType; 51 | private _currentFilesName: string; 52 | private _currentFileWasModified: boolean; 53 | private _onAnyFileModified: any; 54 | private _onActiveLeafChanged: any; 55 | private _onAnyFileAdded: any; 56 | private _onAnyFileRemoved: any; 57 | private _onAnyFileRenamed: any; 58 | 59 | private constructor_internal( 60 | filenames: Array, refreshFnc: Function, onFileRemoved: Function, 61 | fileOrderImportant: boolean): void 62 | { 63 | // The callback for when monitored files have triggered a refresh 64 | this._refreshFnc = refreshFnc; 65 | 66 | // A callback for when files are removed from the list 67 | this._onFileRemoved = onFileRemoved; 68 | 69 | // If true, changes to the order of the files trigger a refresh (just like file changes do) 70 | this._fileOrderImportant = fileOrderImportant; 71 | 72 | // The list of files this Dfc monitors 73 | this._fileData = {}; 74 | 75 | // This var determines What need to happen to monitored files to trigger calling refreshFnc. 76 | // Note - DfcMonitorType: None, OnModify or OnTouch). It has a setter. 77 | this._monitorType = DfcMonitorType.None; 78 | 79 | // Maintain the current active-file, so that when "active-leaf-change" hits (i.e. the 80 | // active-file is set to a different file) you still have access to the prior active-file. 81 | this._currentFilesName = this.getApp().workspace.getActiveFile()?.path ?? ""; 82 | 83 | // Flag set when the current file is modified. 84 | this._currentFileWasModified = false; 85 | 86 | // Setup bound versions of these functions for persistent use 87 | this._onAnyFileModified = this.onAnyFileModified.bind(this); 88 | this._onActiveLeafChanged = this.onActiveLeafChanged.bind(this); 89 | this._onAnyFileAdded = this.onAnyFileAdded.bind(this); 90 | this._onAnyFileRemoved = this.onAnyFileRemoved.bind(this); 91 | this._onAnyFileRenamed = this.onAnyFileRenamed.bind(this); 92 | 93 | // Delay setting up the monitored files list, since it WILL trigger a refreshFnc 94 | // call, and refreshFnc might expect this Dfc to already be assigned to a variable, 95 | // which it won't be until AFTER this constructor is finished. 96 | setTimeout(() => 97 | { 98 | this.updateFileList(filenames, true); 99 | }, 0); 100 | } 101 | 102 | private getApp(): any 103 | { 104 | return InlineScriptsPlugin.getInstance().app; 105 | } 106 | 107 | private setMonitorType_internal(monitorType: DfcMonitorType): void 108 | { 109 | if (monitorType === this._monitorType) { return; } 110 | 111 | const app: any = this.getApp(); 112 | 113 | // At Obsidian start, some Obsidian events trigger haphazardly. We use 114 | // onLayoutReady to wait to connect to the events until AFTER the random triggering 115 | // has passed. 116 | app.workspace.onLayoutReady(() => 117 | { 118 | // React to old monitor type 119 | if (this._monitorType !== DfcMonitorType.None) 120 | { 121 | app.vault.off("modify", this._onAnyFileModified); 122 | app.workspace.off("active-leaf-change", this._onActiveLeafChanged); 123 | app.vault.off("create", this._onAnyFileAdded); 124 | app.vault.off("delete", this._onAnyFileRemoved); 125 | app.vault.off("rename", this._onAnyFileRenamed); 126 | } 127 | 128 | this._monitorType = monitorType; 129 | 130 | // React to new monitor type 131 | if (this._monitorType !== DfcMonitorType.None) 132 | { 133 | app.vault.on("modify", this._onAnyFileModified); 134 | app.workspace.on("active-leaf-change", this._onActiveLeafChanged); 135 | app.vault.on("create", this._onAnyFileAdded); 136 | app.vault.on("delete", this._onAnyFileRemoved); 137 | app.vault.on("rename", this._onAnyFileRenamed); 138 | } 139 | 140 | // Update Dfc state to monitor the active file 141 | this._currentFilesName = app.workspace.getActiveFile()?.path ?? ""; 142 | }); 143 | } 144 | 145 | // Monitor when the current file is modified. If it is, turn on "active leaf changed" 146 | // event to handle refreshFnc call. 147 | private onAnyFileModified(file: any): void 148 | { 149 | // Ignore unmonitored files 150 | if (!this._fileData[file.path]) { return; } 151 | 152 | // If current file was modified, remember to call refreshFnc when leaving the file 153 | // the file 154 | if (file.path === this._currentFilesName) 155 | { 156 | this._currentFileWasModified = true; 157 | } 158 | 159 | // If non-current file was modified, call refreshFnc immediately 160 | else 161 | { 162 | this.refresh(true); 163 | } 164 | } 165 | 166 | // Monitor when a different file becomes the active one. If the prior active file is one 167 | // of the files being monitored then this can trigger a refreshFnc call. 168 | private onActiveLeafChanged(): void 169 | { 170 | // Ignore unmonitored files 171 | if (this._fileData[this._currentFilesName]) 172 | { 173 | // If leaving a file and it was changed, or monitorType === OnTouch, refresh 174 | if (this._currentFileWasModified || this._monitorType === DfcMonitorType.OnTouch) 175 | { 176 | this.refresh(true); 177 | } 178 | } 179 | 180 | // Update Dfc state to monitor the active file 181 | this._currentFileWasModified = false; 182 | this._currentFilesName = this.getApp().workspace.getActiveFile()?.path ?? ""; 183 | } 184 | 185 | // Monitor when files are added to the vault. If the file is one of the ones being monitored, 186 | // refreshFnc is called. 187 | private onAnyFileAdded(file: any): void 188 | { 189 | // Refresh if file is being monitored 190 | if (!this._fileData[file.path]) { return; } 191 | this.refresh(true); 192 | } 193 | 194 | // Monitor when files are removed from the vault. If the file is one of the ones being 195 | // monitored, refreshFnc is called. 196 | private onAnyFileRemoved(file: any): void 197 | { 198 | // Refresh if file is being monitored 199 | if (!this._fileData[file.path]) { return; } 200 | this.refresh(true); 201 | 202 | // Call the onFileRemoved callback 203 | if (this._onFileRemoved) 204 | { 205 | this._onFileRemoved(file.path); 206 | } 207 | } 208 | 209 | // Monitor when files are renamed. 210 | // If the renamed file is the current file, update _currentFilesName. 211 | private onAnyFileRenamed(file: any): void 212 | { 213 | const app: any = this.getApp(); 214 | if (file === app.workspace.getActiveFile()) 215 | { 216 | this._currentFilesName = app.workspace.getActiveFile()?.path ?? ""; 217 | } 218 | } 219 | 220 | public updateFileList_internal(newFileList: Array, forceRefresh?: boolean) : void 221 | { 222 | let hasChanged: boolean = false; 223 | 224 | // Synchronize this._fileData with newFileList 225 | for (const filename in this._fileData) 226 | { 227 | if (!newFileList.includes(filename)) 228 | { 229 | if (this._onFileRemoved) 230 | { 231 | this._onFileRemoved(filename); 232 | } 233 | delete this._fileData[filename]; 234 | hasChanged = true; 235 | } 236 | } 237 | for (const newFile of newFileList) 238 | { 239 | if (!this._fileData.hasOwnProperty(newFile)) 240 | { 241 | this._fileData[newFile] = { modDate: Number.MIN_SAFE_INTEGER }; 242 | if (this._fileOrderImportant) 243 | { 244 | this._fileData[newFile].ordering = -1; 245 | } 246 | hasChanged = true; 247 | } 248 | } 249 | 250 | // Check changes to file order 251 | if (this._fileOrderImportant) 252 | { 253 | for (let i: number = 0; i < newFileList.length; i++) 254 | { 255 | if (this._fileData[newFileList[i]].ordering !== i) 256 | { 257 | this._fileData[newFileList[i]].ordering = i; 258 | hasChanged = true; 259 | } 260 | } 261 | } 262 | 263 | this.refresh(hasChanged || forceRefresh); 264 | } 265 | 266 | // Calls refreshFnc if warranted. refreshFnc is the callback for when monitored files 267 | // require a refresh. This calls refreshFnc either when forceRefresh is true, or if one or 268 | // more of the monitored files have changed (i.e. their modified date has changed). 269 | private refresh(forceRefresh?: boolean): void 270 | { 271 | const app: any = this.getApp(); 272 | app.workspace.onLayoutReady(async () => 273 | { 274 | let hasChanged: boolean = false; 275 | 276 | // If forceRefresh, then we know we're going to call refreshFnc, but we 277 | // still need check modified dates to keep our records up to date. 278 | for (const filename in this._fileData) 279 | { 280 | const file: any = app.vault.fileMap[filename]; 281 | 282 | // If file exists... 283 | if (file) 284 | { 285 | // Check mod-date. If newer then recorded, record new 286 | // mod-date and that refreshFnc should be called 287 | if (this._fileData[filename].modDate < file.stat.mtime) 288 | { 289 | this._fileData[filename].modDate = file.stat.mtime; 290 | hasChanged = true; 291 | } 292 | } 293 | 294 | // If file doesn't exist, but a valid mod-date is recorded for it, 295 | // invalidate mod-date and record that refreshFnc should be called 296 | else if (this._fileData[filename].modDate !== Number.MIN_SAFE_INTEGER) 297 | { 298 | this._fileData[filename].modDate = Number.MIN_SAFE_INTEGER; 299 | hasChanged = true; 300 | } 301 | } 302 | 303 | // call refreshFnc if a file has changed, or refresh is being forced 304 | if ((hasChanged || forceRefresh) && this._refreshFnc) 305 | { 306 | this._refreshFnc(); 307 | } 308 | }); 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /src/ExternalRunner.ts: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////////////////// 2 | // External runner - Functionality to run shell commands (non-mobile only) // 3 | ///////////////////////////////////////////////////////////////////////////// 4 | 5 | "use strict"; 6 | 7 | import { Platform } from "obsidian"; 8 | import InlineScriptsPlugin from "./_Plugin"; 9 | import { UserNotifier } from "./ui_userNotifier"; 10 | 11 | let exec: any = null; 12 | 13 | export namespace ExternalRunner 14 | { 15 | export async function run( 16 | command: string, failSilently?: boolean, dontFixSlashes?: boolean): Promise 17 | { 18 | // Error-out if on mobile platform 19 | if (Platform.isMobile) 20 | { 21 | UserNotifier.run( 22 | { 23 | popupMessage: "Unauthorized \"runExternal\" call", 24 | consoleMessage: "Unauthorized \"runExternal\" call (not available on mobile):\n" + 25 | "runExternal(\"" + command + "\")", 26 | messageType: "RUNEXTERNAL-ERROR", 27 | consoleHasDetails: true 28 | }); 29 | return null; 30 | } 31 | else if (!exec) 32 | { 33 | try 34 | { 35 | exec = require("util").promisify(require("child_process").exec); 36 | } 37 | catch(e: any) 38 | { 39 | console.error("External runner failed to load \"child_process\": " + e); 40 | } 41 | } 42 | 43 | const plugin = InlineScriptsPlugin.getInstance(); 44 | 45 | // Error-out if runExternal is not explicitly allowed by the user. 46 | // note - User allows runExternal by turning on the toggle "Allow external" in the settings. 47 | if (!plugin.settings.allowExternal) 48 | { 49 | UserNotifier.run( 50 | { 51 | popupMessage: "Unauthorized \"runExternal\" call", 52 | consoleMessage: "Unauthorized \"runExternal\" call (disallowed by user):\n" + 53 | "runExternal(\"" + command + "\")\nNOTE: User can allow runExternal by turning " + 54 | "on \"Allow external\" in the settings.", 55 | messageType: "RUNEXTERNAL-ERROR", 56 | consoleHasDetails: true 57 | }); 58 | return null; 59 | } 60 | 61 | // Fail if command is empty 62 | if (!command) { return null; } 63 | 64 | // Slashes on a Windows platform need reversing (to blackslash). 65 | if (navigator.appVersion.includes("Windows") && !dontFixSlashes) 66 | { 67 | command = command.replaceAll("/", "\\"); 68 | } 69 | 70 | // Run the shell command 71 | const vaultDir: string = (plugin.app.fileManager as any).vault.adapter.basePath; 72 | try 73 | { 74 | const result: string = (await exec(command, { cwd: vaultDir })).stdout; 75 | return (result + "").replaceAll("\r", ""); 76 | } 77 | 78 | // Handle errors from running the shell command 79 | catch (e: any) 80 | { 81 | if (!failSilently) 82 | { 83 | UserNotifier.run( 84 | { 85 | popupMessage: "Failed \"runExternal\" call", 86 | consoleMessage: 87 | "Failed \"runExternal\" call:\ncurDir: " + vaultDir + "\n" + e.message, 88 | messageType: "RUNEXTERNAL-ERROR", 89 | consoleHasDetails: true 90 | }); 91 | } 92 | return null; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/HelperFncs.ts: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////// 2 | // HelperFncs - Useful functions that don't fit into a different ts file // 3 | /////////////////////////////////////////////////////////////////////////// 4 | 5 | "use strict"; 6 | 7 | import { MarkdownRenderer, ItemView, addIcon } from "obsidian"; 8 | import { DragReorder } from "./ui_dragReorder"; 9 | import { InputBlocker } from "./ui_InputBlocker"; 10 | import { ExternalRunner } from "./ExternalRunner"; 11 | import { UserNotifier } from "./ui_userNotifier"; 12 | import { Popups } from "./ui_Popups"; 13 | import InlineScriptsPlugin from "./_Plugin"; 14 | 15 | /*! getEmPixels | Author: Tyson Matanich (http://matanich.com), 2013 | License: MIT */ 16 | (function(n,t){"use strict";var i="!important;",r="position:absolute"+i+"visibility:hidden"+i+"width:1em"+i+"font-size:1em"+i+"padding:0"+i;window.getEmPixels=function(u:any){var f,e,o;return u||(u=f=n.createElement("body"),f.style.cssText="font-size:1em"+i,t.insertBefore(f,n.body)),e=n.createElement("i"),e.style.cssText=r,u.appendChild(e),o=e.clientWidth,f?t.removeChild(f):u.removeChild(e),o}})(document,document.documentElement); 17 | 18 | export namespace HelperFncs 19 | { 20 | export function staticConstructor(): void 21 | { 22 | confirmObjectPath("_inlineScripts.inlineScripts.HelperFncs"); 23 | Object.assign(window._inlineScripts.inlineScripts.HelperFncs, 24 | { 25 | runExternal: ExternalRunner.run, print: UserNotifier.getFunction_print(), 26 | popups: Popups.getInstance(), 27 | confirmObjectPath, getLeavesForFile, addToNote, parseMarkdown, 28 | callEventListenerCollection, addCss, removeCss, ItemView, addIcon, DragReorder, unblock, 29 | expFormat, expUnformat, getSettings, registerView, fileWrite, asyncFilter, asyncMap, 30 | asyncForEach 31 | }); 32 | } 33 | 34 | export function versionCompare(version1: string, version2: string): number 35 | { 36 | return versionCompare_internal(version1, version2); 37 | } 38 | 39 | export async function fileWrite(filepath: string, content: string): Promise 40 | { 41 | await fileWrite_internal(filepath, content); 42 | } 43 | 44 | // confirm that an object path is available 45 | export function confirmObjectPath(path: string, leaf?: any): void 46 | { 47 | confirmObjectPath_internal(path, leaf); 48 | } 49 | 50 | export function getLeavesForFile(file: any): Array 51 | { 52 | return getLeavesForFile_internal(file); 53 | } 54 | 55 | // Takes some text and places it into the current note, replacing the text at targetPosition, 56 | // if it is assigned, or appending to the document end if not. 57 | export async function addToNote(toAdd: string, targetPosition?: any, path?: string): 58 | Promise 59 | { 60 | await addToNote_internal(toAdd, targetPosition, path); 61 | } 62 | 63 | export function parseMarkdown(md: string): string 64 | { 65 | return parseMarkdown_internal(md); 66 | } 67 | 68 | export async function callEventListenerCollection( 69 | title: string, collection: any, parameters?: any, onReturn?: Function): Promise 70 | { 71 | await callEventListenerCollection_internal(title, collection, parameters, onReturn); 72 | } 73 | 74 | export function addCss(id: string, css: string): void 75 | { 76 | addCss_internal(id, css); 77 | } 78 | 79 | export function removeCss(id: string): void 80 | { 81 | removeCss_internal(id); 82 | } 83 | 84 | export function unblock(): void 85 | { 86 | InputBlocker.setEnabled(false); 87 | } 88 | 89 | // Modify a string ("expansion") to add the expansion format: prefix, lineprefix and suffix 90 | export function expFormat( 91 | expansion: string, skipPrefix: boolean, skipLinePrefix: boolean, skipSuffix: boolean) 92 | : string 93 | { 94 | return expFormat_internal(expansion, skipPrefix, skipLinePrefix, skipSuffix); 95 | } 96 | 97 | // Modify a string ("expansion") to remove the expansion format: prefix, lineprefix and suffix 98 | export function expUnformat( 99 | expansion: string, skipPrefix: boolean, skipLinePrefix: boolean, skipSuffix: boolean) 100 | : string 101 | { 102 | return expUnformat_internal(expansion, skipPrefix, skipLinePrefix, skipSuffix); 103 | } 104 | 105 | // Return a copy of the current settings object 106 | export function getSettings(): any 107 | { 108 | return Object.assign({}, InlineScriptsPlugin.getInstance().settings); 109 | } 110 | 111 | // Wraps the plugin's registerView functionality 112 | export function registerView(id: string, viewCreator: any) 113 | { 114 | return InlineScriptsPlugin.getInstance().registerView(id, viewCreator); 115 | } 116 | 117 | // Equivalent to array.filter, except that the passed function is run asynchronously 118 | export async function asyncFilter(arr: Array, fnc: any): Promise> 119 | { 120 | return await asyncFilter_internal(arr, fnc); 121 | } 122 | 123 | // Equivalent to array.map, except that the passed function is run asynchronously 124 | export async function asyncMap(arr: Array, fnc: any): Promise> 125 | { 126 | return await asyncMap_internal(arr, fnc); 127 | } 128 | 129 | // Equivalent to array.forEach, except that the passed function is run asynchronously 130 | export async function asyncForEach(arr: Array, fnc: any): Promise 131 | { 132 | return await asyncForEach_internal(arr, fnc); 133 | } 134 | 135 | /////////////////////////////////////////////////////////////////////////////////////////////////// 136 | 137 | function versionCompare_internal(version1: string, version2: string): number 138 | { 139 | const convert = 140 | (v: string) => v.split(".").map((x: string) => x.padStart(5, "0")).join("."); 141 | version1 = convert(version1); 142 | version2 = convert(version2); 143 | return version1.localeCompare(version2); 144 | }; 145 | 146 | async function fileWrite_internal(filepath: string, content: string): Promise 147 | { 148 | const plugin = InlineScriptsPlugin.getInstance(); 149 | const file: any = (plugin.app.vault as any).fileMap[filepath]; 150 | if (file) 151 | { 152 | await plugin.app.vault.modify(file, content); 153 | } 154 | else 155 | { 156 | await plugin.app.vault.create(filepath, content); 157 | } 158 | } 159 | 160 | function confirmObjectPath_internal(path: string, leaf?: any): void 161 | { 162 | const pathChain = path.split("."); 163 | let parent: any = window; 164 | for (let i = 0; i < pathChain.length-1; i++) 165 | { 166 | parent = (parent[pathChain[i]] ||= {}); 167 | } 168 | parent[pathChain[pathChain.length-1]] ||= (leaf === undefined ? {} : leaf); 169 | } 170 | 171 | function getLeavesForFile_internal(file: any): Array 172 | { 173 | let result = []; 174 | for (const leaf of 175 | InlineScriptsPlugin.getInstance().app.workspace.getLeavesOfType("markdown")) 176 | { 177 | if ((leaf.view as any)?.file === file) 178 | { 179 | result.push(leaf); 180 | } 181 | } 182 | return result; 183 | } 184 | 185 | async function addToNote_internal(toAdd: string, targetPosition?: any, path?: string) 186 | : Promise 187 | { 188 | // targetPosition defaults to last position possible 189 | targetPosition ||= { start: Number.MAX_SAFE_INTEGER, end: Number.MAX_SAFE_INTEGER }; 190 | 191 | const plugin = InlineScriptsPlugin.getInstance(); 192 | 193 | // Get file object for the file to edit 194 | const file = 195 | !path ? plugin.app.workspace.getActiveFile() : (plugin.app.vault as any).fileMap[path]; 196 | if (!file || file.children) { return; } 197 | 198 | // Check if we're editing the ACTIVE file 199 | let isNoteActive = (!path || file === plugin.app.workspace.getActiveFile()); 200 | 201 | const leaves = HelperFncs.getLeavesForFile(file); 202 | const currentMode = leaves[0]?.view?.currentMode; 203 | 204 | if (isNoteActive && currentMode?.type === "source") 205 | { 206 | plugin.app.workspace.setActiveLeaf(leaves[0], false, true); 207 | } 208 | 209 | if (!toAdd) { return; } 210 | if (Array.isArray(toAdd)) 211 | { 212 | toAdd = toAdd.join(""); 213 | } 214 | 215 | if (!leaves.length) 216 | { 217 | // Read the content 218 | let content = await plugin.app.vault.cachedRead(file); 219 | 220 | // Modify the content 221 | content = 222 | content.slice(0, targetPosition.start) + toAdd + content.slice(targetPosition.end); 223 | 224 | // Write the content back 225 | await plugin.app.vault.modify(file, content); 226 | } 227 | else if (currentMode.type === "source") 228 | { 229 | // Temporarily remove plugin input blocking (since disabling it breaks the note editing) 230 | const inputDisabled = plugin.inputDisabled; 231 | plugin.inputDisabled = false; 232 | 233 | // Append to the editor 234 | let content = leaves[0].view.editor.getValue(); 235 | const oldContentSize = content.length; 236 | content = 237 | content.slice(0, targetPosition.start) + toAdd + content.slice(targetPosition.end); 238 | await leaves[0].view.editor.setValue(content); 239 | 240 | // Restore plugin input blocking 241 | plugin.inputDisabled = inputDisabled; 242 | 243 | // Move caret to the note's end (only if editing the active note & edit is at the end of file) 244 | if (isNoteActive && targetPosition.start >= oldContentSize) 245 | { 246 | const scroller = currentMode?.contentContainerEl?.parentElement; 247 | if (scroller) 248 | { 249 | const oldScrollTop = scroller.scrollTop; 250 | leaves[0].view.editor.setSelection({line: Number.MAX_SAFE_INTEGER, ch: 0}); 251 | setTimeout(() => 252 | { 253 | if (scroller.scrollTop != oldScrollTop) 254 | { 255 | scroller.scrollTop += window.getEmPixels(scroller) * 2; 256 | } 257 | }, 100); 258 | } 259 | else 260 | { 261 | leaves[0].view.editor.setSelection({line: Number.MAX_SAFE_INTEGER, ch: 0}); 262 | } 263 | } 264 | } 265 | else 266 | { 267 | // Read the content 268 | let content = leaves[0].view.data; 269 | const oldContentSize = content.length; 270 | 271 | // Modify the content 272 | content = 273 | content.slice(0, targetPosition.start) + toAdd + content.slice(targetPosition.end); 274 | 275 | // Write the content back 276 | await plugin.app.vault.modify(file, content); 277 | 278 | // Scroll to note's end (only if editing the active note & edit is at the end of file) 279 | if (isNoteActive && targetPosition.start >= oldContentSize) 280 | { 281 | const scroller = currentMode.containerEl.childNodes[0]; 282 | const scrollerChild = scroller.childNodes[0]; 283 | const paddingBottom = scrollerChild.style["padding-bottom"]; 284 | scrollerChild.style["padding-bottom"] = 0; 285 | setTimeout(() => 286 | { 287 | scroller.scrollTop = scroller.scrollHeight; 288 | scrollerChild.style["padding-bottom"] = paddingBottom; 289 | }, 100); 290 | } 291 | } 292 | } 293 | 294 | function parseMarkdown_internal(md: string): string 295 | { 296 | const ui = document.createElement("div"); 297 | MarkdownRenderer.renderMarkdown(md, ui, '', null); 298 | let result = ui.innerHTML; 299 | if (result.startsWith("

") && result.endsWith("

")) 300 | { 301 | result = result.slice(3, -4); 302 | } 303 | return result; 304 | } 305 | 306 | async function callEventListenerCollection_internal( 307 | title: string, collection: any, parameters?: any, onReturn?: Function): Promise 308 | { 309 | if (!collection) 310 | { 311 | return; 312 | } 313 | let toCall: any = 314 | Object.keys(collection).map(v => { return {key: v, fnc: collection[v]}; }); 315 | const sfileIndices = window._inlineScripts.inlineScripts.sfileIndices; 316 | for (const toCallItem of toCall) 317 | { 318 | if (sfileIndices[toCallItem.key]) 319 | { 320 | toCallItem.key = 321 | (sfileIndices[toCallItem.key]+"").padStart(3, "0") + toCallItem.key; 322 | } 323 | } 324 | toCall = 325 | toCall 326 | .sort((lhs: any, rhs: any) => { return lhs.key.localeCompare(rhs.key); }) 327 | .map((v: any) => v.fnc); 328 | 329 | for (const fnc of toCall) 330 | { 331 | if (typeof fnc === "function") 332 | { 333 | const result = await fnc(parameters); 334 | if (result != undefined && onReturn) 335 | { 336 | onReturn(result); 337 | } 338 | } 339 | else 340 | { 341 | console.warn("Non-function in collection \"" + title + "\": " + fnc); 342 | } 343 | } 344 | } 345 | 346 | function addCss_internal(id: string, css: string): void 347 | { 348 | id = id + "_css"; 349 | let e = document.getElementById(id); 350 | if (!e) 351 | { 352 | e = document.createElement("style"); 353 | e.id = id; 354 | document.head.appendChild(e); 355 | } 356 | e.innerText = css; 357 | } 358 | 359 | function removeCss_internal(id: string): void 360 | { 361 | id = id + "_css"; 362 | const e = document.getElementById(id); 363 | e?.remove(); 364 | } 365 | 366 | function expFormat_internal( 367 | expansion: string, skipPrefix: boolean, skipLinePrefix: boolean, skipSuffix: boolean) 368 | : string 369 | { 370 | // Used on all prefixes and suffixes to allow user to specify newlines, tabs and quotes. 371 | function unescapeText(src: string) 372 | { 373 | return src.replaceAll("\\n", "\n").replaceAll("\\t", "\t").replaceAll("\\\"", "\""); 374 | } 375 | 376 | // Expansion can be a string or an array-of-strings. If expansion is NOT an 377 | // array-of-strings, make it an array-of-strings, temporarily, to simplify formatting logic. 378 | let result = Array.isArray(expansion) ? expansion : [ expansion ]; 379 | 380 | const settings = InlineScriptsPlugin.getInstance().settings; 381 | 382 | // linePrefix handling - @ start of result[0] & after each newline in all result elements. 383 | if (!skipLinePrefix) 384 | { 385 | const linePrefix = unescapeText(settings.expansionLinePrefix); 386 | result[0] = linePrefix + result[0]; 387 | for (let i = 0; i < result.length; i++) 388 | { 389 | if (!result[i].replaceAll) { continue; } 390 | result[i] = result[i].replaceAll("\n", "\n" + linePrefix); 391 | } 392 | } 393 | 394 | // Prefix handling - at start of first element 395 | if (!skipPrefix) 396 | { 397 | const prefix = unescapeText(settings.expansionPrefix); 398 | result[0] = prefix + result[0]; 399 | } 400 | 401 | // Suffix handling - after end of last element 402 | if (!skipSuffix) 403 | { 404 | const suffix = unescapeText(settings.expansionSuffix); 405 | result[result.length-1] = result[result.length-1] + suffix; 406 | } 407 | 408 | // If passed expansion wasn't an array, turn result back into a non-array. 409 | return Array.isArray(expansion) ? result : result[0]; 410 | } 411 | 412 | function expUnformat_internal( 413 | expansion: string, skipPrefix: boolean, skipLinePrefix: boolean, skipSuffix: boolean) 414 | : string 415 | { 416 | // Used on all prefixes and suffixes to allow user to specify newlines, tabs and quotes. 417 | function unescapeText(src: string) 418 | { 419 | return src.replaceAll("\\n", "\n").replaceAll("\\t", "\t").replaceAll("\\\"", "\""); 420 | } 421 | 422 | // Expansion can be a string or an array-of-strings. If expansion is NOT an 423 | // array-of-strings, make it an array-of-strings, temporarily, to simplify formatting logic. 424 | let result = Array.isArray(expansion) ? expansion : [ expansion ]; 425 | 426 | const settings = InlineScriptsPlugin.getInstance().settings; 427 | 428 | // Prefix handling - at start of first element 429 | if (!skipPrefix) 430 | { 431 | const prefix = unescapeText(settings.expansionPrefix); 432 | result[0] = result[0].replace(new RegExp("^" + prefix), ""); 433 | } 434 | 435 | // Suffix handling - after end of last element 436 | if (!skipSuffix) 437 | { 438 | const suffix = unescapeText(settings.expansionSuffix); 439 | result[result.length-1] = result[result.length-1].replace(new RegExp(suffix + "$"), ""); 440 | } 441 | 442 | // linePrefix handling - @ start of result[0] & after each newline in all result elements. 443 | if (!skipLinePrefix) 444 | { 445 | const linePrefix = unescapeText(settings.expansionLinePrefix); 446 | result[0] = result[0].replace(new RegExp("^" + linePrefix), ""); 447 | for (let i = 0; i < result.length; i++) 448 | { 449 | if (!result[i].replaceAll) { continue; } 450 | result[i] = result[i].replaceAll("\n" + linePrefix, "\n"); 451 | } 452 | } 453 | 454 | // If passed expansion wasn't an array, turn result back into a non-array. 455 | return Array.isArray(expansion) ? result : result[0]; 456 | } 457 | 458 | // Equivalent to array.filter, except that the passed function is run asynchronously 459 | async function asyncFilter_internal(arr: Array, fnc: any): Promise> 460 | { 461 | const predicateResults = await Promise.all(arr.map(fnc)); 462 | return arr.filter((v, i) => predicateResults[i]); 463 | } 464 | 465 | // Equivalent to array.map, except that the passed function is run asynchronously 466 | async function asyncMap_internal(arr: Array, fnc: any): Promise> 467 | { 468 | return await Promise.all(arr.map(fnc)); 469 | } 470 | 471 | // Equivalent to array.forEach, except that the passed function is run asynchronously 472 | async function asyncForEach_internal(arr: Array, fnc: any): Promise 473 | { 474 | for (let i = 0; i < arr.length; i++) 475 | { 476 | await fnc(arr[i], i, arr); 477 | } 478 | } 479 | } 480 | -------------------------------------------------------------------------------- /src/LibraryImporter.ts: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////// 2 | // Library Importer - Pulls the official library from github & adds it to the current vault // 3 | ////////////////////////////////////////////////////////////////////////////////////////////// 4 | 5 | "use strict"; 6 | 7 | import { normalizePath } from "obsidian"; 8 | import InlineScriptsPlugin from "./_Plugin"; 9 | import { UserNotifier } from "./ui_userNotifier"; 10 | import { InputBlocker } from "./ui_InputBlocker"; 11 | import { Popups } from "./ui_Popups"; 12 | import { HelperFncs } from "./HelperFncs"; 13 | import { SettingUi_ShortcutFiles } from "./ui_setting_shortcutFiles"; 14 | 15 | const REGEX_LIBRARY_README_SHORTCUT_FILE: RegExp = 16 | /### ([_a-zA-Z0-9]+.sfile)\n(_\(disabled by default)?/g; 17 | const DEFAULT_REMOTE_ADDRESS: string = 18 | "https://raw.githubusercontent.com/jon-heard/" + 19 | "obsidian-inline-scripts-library/main"; 20 | const DEFAULT_LOCAL_ADDRESS: string = "support/inlineScripts"; 21 | const FILE_README: string = "README.md"; 22 | const PRE_REFACTOR_SFILES = ["tejs_state","tejs_lists","tejs_mythicv2","tejs_mythicgme","tejs_une","tejs_adventurecrafter","tejs_rpgtools","tejs_clips","tejs_arrows","tejs_lipsum","tejs_support"]; 23 | 24 | export namespace LibraryImporter 25 | { 26 | // Pull the official library from github & add it to the current vault 27 | export async function run(): Promise 28 | { 29 | return await run_internal(); 30 | } 31 | 32 | /////////////////////////////////////////////////////////////////////////////////////////////////// 33 | 34 | async function run_internal(useCustomSource?: boolean): Promise 35 | { 36 | const plugin = InlineScriptsPlugin.getInstance(); 37 | 38 | // Need to manually disable user-input until this process is finished 39 | // (due to asynchronous downloads not otherwise blocking user-input) 40 | InputBlocker.setEnabled(true); 41 | 42 | let addressRemote: string = DEFAULT_REMOTE_ADDRESS; 43 | 44 | // Give the option to change the library source 45 | if (useCustomSource) 46 | { 47 | addressRemote = await Popups.getInstance().input( 48 | "What is the library source?", DEFAULT_REMOTE_ADDRESS); 49 | if (addressRemote === null) 50 | { 51 | InputBlocker.setEnabled(false); 52 | return false; 53 | } 54 | } 55 | 56 | // Get list of shortcut-files from the project's github readme. Sanitize the newlines. 57 | let readmeContent: string; 58 | try 59 | { 60 | readmeContent = await window.request({ 61 | url: addressRemote + "/" + FILE_README, 62 | method: "GET", headers: { "Cache-Control": "no-cache" } 63 | }); 64 | } 65 | catch(e: any) 66 | { 67 | UserNotifier.run({ 68 | popupMessage: "Library importing failed.\nUnable to connect.", 69 | consoleMessage: "Library importing failed.", 70 | messageType: e.message 71 | }); 72 | InputBlocker.setEnabled(false); 73 | return false; 74 | } 75 | readmeContent = readmeContent.replaceAll("\r", ""); 76 | let libShortcutFiles: Array = []; 77 | let disabledShortcutFiles: Array = []; 78 | for (const match of readmeContent.matchAll(REGEX_LIBRARY_README_SHORTCUT_FILE)) 79 | { 80 | libShortcutFiles.push(match[1]); 81 | if (match[2]) 82 | { 83 | disabledShortcutFiles.push(match[1]); 84 | } 85 | } 86 | 87 | // Sometimes we should check both library files AND pre-refactor library files 88 | const libSFiles_currentAndPrerefactor = libShortcutFiles.concat(PRE_REFACTOR_SFILES); 89 | 90 | // Pick default library path. This is normally ADDRESSS_LOCAL. But, if all shortcut-file 91 | // entries that match library files are in a single folder, use that instead. 92 | const sfNoteAddresses: Array = 93 | SettingUi_ShortcutFiles.getContents().shortcutFiles.map((f: any) => f.address); 94 | // The filenames of referenced shortcut-files 95 | const sfNoteNames: Array = 96 | sfNoteAddresses.map(s => s.slice(s.lastIndexOf("/")+1, -3)); 97 | // The paths of referenced shortcut-files 98 | const sfNotePaths: Array = sfNoteAddresses.map((s: any, i: number) => 99 | { 100 | return s.slice(0, s.length-sfNoteNames[i].length-4) 101 | }); 102 | // Find a common path, or lack thereof, to shortcut-files belonging to the library 103 | let commonPath: string = null; 104 | for (let i: number = 0; i < sfNoteAddresses.length; i++) 105 | { 106 | if(libSFiles_currentAndPrerefactor.includes(sfNoteNames[i])) 107 | { 108 | if (commonPath === null) 109 | { 110 | commonPath = sfNotePaths[i]; 111 | } 112 | else 113 | { 114 | if (sfNotePaths[i] !== commonPath) 115 | { 116 | commonPath = null; 117 | break; 118 | } 119 | } 120 | } 121 | } 122 | 123 | // Have user pick the library path, using the default determined above. Cancel ends import. 124 | const libDstSuggestions = 125 | Object.keys((plugin.app.vault as any).fileMap) 126 | .filter(v => (plugin.app.vault as any).fileMap[v].children) 127 | .filter(v => (v !== "/")); 128 | let libraryDestinationPath: string = await Popups.getInstance().input( 129 | "What path should the library be placed in?", commonPath || DEFAULT_LOCAL_ADDRESS, 130 | libDstSuggestions); 131 | if (libraryDestinationPath === null) 132 | { 133 | InputBlocker.setEnabled(false); 134 | return false; 135 | } 136 | 137 | if (libraryDestinationPath.trim().toLowerCase() === "customlibsrc") 138 | { 139 | return run_internal(true); 140 | } 141 | 142 | // Normalize the inputted library destination path 143 | libraryDestinationPath = normalizePath(libraryDestinationPath); 144 | 145 | // Adjust the disabledShortcutFiles to match the libraryDestinationPath 146 | disabledShortcutFiles = 147 | disabledShortcutFiles.map(v => libraryDestinationPath + "/" + v + ".md"); 148 | 149 | // Create the choosen library destination folder, if necessary 150 | if (!(plugin.app.vault as any).fileMap.hasOwnProperty(libraryDestinationPath)) 151 | { 152 | await plugin.app.vault.createFolder(libraryDestinationPath); 153 | } 154 | 155 | // Download and create library files 156 | for (const libShortcutFile of libShortcutFiles) 157 | { 158 | // Download the file 159 | const content: string = await window.request({ 160 | url: addressRemote + "/" + libShortcutFile + ".md", 161 | method: "GET", headers: { "Cache-Control": "no-cache" } 162 | }); 163 | 164 | const filename: string = libraryDestinationPath + "/" + libShortcutFile + ".md"; 165 | await HelperFncs.fileWrite(filename, content); 166 | } 167 | 168 | // Delete any pre-refactor library files in the shortcut-files list 169 | for (let i = 0; i < sfNoteAddresses.length; i++) 170 | { 171 | if (PRE_REFACTOR_SFILES.includes(sfNoteNames[i])) 172 | { 173 | await plugin.app.vault.delete((plugin.app.vault as any).fileMap[sfNoteAddresses[i]]); 174 | } 175 | } 176 | 177 | // Add version file 178 | { 179 | const libVersion = readmeContent.match(/# Version (.*)/)[1] || ""; 180 | const filename: string = libraryDestinationPath + "/" + "Ξ_libraryVersion.md"; 181 | await HelperFncs.fileWrite(filename, libVersion); 182 | } 183 | 184 | // Before adding the library shortcut-files to the plugin settings, we should 185 | // update the plugin settings with the latest changes made in the settings ui. 186 | plugin.settings.shortcutFiles = SettingUi_ShortcutFiles.getContents().shortcutFiles; 187 | 188 | // We don't want to duplicate shortcut-files, and it's important to keep the library 189 | // shortcut-files in-order. Remove any shortcut-files from the list that are part of the 190 | // library before appending the shortcut-files from the library to the end of the list. 191 | // NOTE - we are replacing some shortcuts from the library, but we do want to keep their 192 | // enable state so the user doesn't have to re-disable unwanted shortcut-files. 193 | for (const libShortcutFile of libSFiles_currentAndPrerefactor) 194 | { 195 | const shortcutFileAddresses = plugin.settings.shortcutFiles.map((f: any) => f.address); 196 | const libAddress: string = libraryDestinationPath + "/" + libShortcutFile + ".md"; 197 | const index: number = shortcutFileAddresses.indexOf(libAddress); 198 | if (index >= 0) 199 | { 200 | if (!plugin.settings.shortcutFiles[index].enabled) 201 | { 202 | disabledShortcutFiles.push(libAddress); 203 | } 204 | plugin.settings.shortcutFiles.splice(index, 1); 205 | } 206 | } 207 | 208 | // Add all library shortcut-files to the settings 209 | for (const libShortcutFile of libShortcutFiles) 210 | { 211 | const address = libraryDestinationPath + "/" + libShortcutFile + ".md"; 212 | plugin.settings.shortcutFiles.push( 213 | { 214 | enabled: !disabledShortcutFiles.includes(address), 215 | address: address 216 | }); 217 | } 218 | 219 | // Refresh settings ui to display the updated list of shortcut-files 220 | InlineScriptsPlugin.getInstance().settingsUi.display(); 221 | InputBlocker.setEnabled(false); 222 | 223 | return true; 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/ShortcutExpander.ts: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////// 2 | // Shortcut expander - Logic to "expand" a shortcut string. // 3 | ////////////////////////////////////////////////////////////// 4 | 5 | "use strict"; 6 | 7 | import InlineScriptsPlugin from "./_Plugin"; 8 | import { UserNotifier } from "./ui_userNotifier"; 9 | import { ExternalRunner } from "./ExternalRunner"; 10 | import { AutoAsyncWrapper } from "./AutoAsyncWrapper"; 11 | import { Parser } from "./node_modules/acorn/dist/acorn"; 12 | import { Popups } from "./ui_Popups"; 13 | import { HelperFncs } from "./HelperFncs"; 14 | 15 | // Get the AsyncFunction constructor to setup and run Expansion scripts with 16 | const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor; 17 | 18 | export abstract class ShortcutExpander 19 | { 20 | public static staticConstructor(): void 21 | { 22 | this.staticConstructor_internal(); 23 | } 24 | 25 | // Take a shortcut string and expand it based on shortcuts active in the plugin 26 | public static async expand( 27 | shortcutText: string, failSilently?: boolean, expansionInfo?: any, 28 | parameterData?: any): Promise 29 | { 30 | return await this.expand_internal(shortcutText, failSilently, expansionInfo, parameterData); 31 | } 32 | 33 | // Execute an expansion script (a string of JavaScript defined in a shortcut's Expansion string) 34 | public static async runExpansionScript( 35 | expansionScript: string, failSilently?: boolean, expansionInfo?: any): Promise 36 | { 37 | return await this.runExpansionScript_internal(expansionScript, failSilently, expansionInfo); 38 | } 39 | 40 | /////////////////////////////////////////////////////////////////////////////////////////////////// 41 | 42 | private static _boundExpand: Function; 43 | 44 | private static staticConstructor_internal(): void 45 | { 46 | // Initialize the AutoAsyncWrapper 47 | AutoAsyncWrapper.initialize([ 48 | "expand", "popups\s*\.\s*alert", "popups\s*\.\s*confirm", "popups\s*\.\s*input", 49 | "popups\s*\.\s*pick", "popups\s*\.\s*custom" ]); 50 | 51 | // Setup bound versons of these function for persistant use 52 | this._boundExpand = this.expand.bind(this); 53 | 54 | // Add "expand()" to "window._inlineScripts.inlineScripts.HelperFncs" 55 | HelperFncs.confirmObjectPath("_inlineScripts.inlineScripts.HelperFncs"); 56 | window._inlineScripts.inlineScripts.HelperFncs.expand = this._boundExpand; 57 | } 58 | 59 | // Take a shortcut string and return the proper Expansion script. 60 | // WARNING: user-facing function 61 | private static async expand_internal( 62 | shortcutText: string, failSilently?: boolean, expansionInfo?: any, 63 | parameterData?: any): Promise 64 | { 65 | if (!shortcutText) { return; } 66 | 67 | expansionInfo = Object.assign( 68 | { 69 | isUserTriggered: false, 70 | line: shortcutText, 71 | inputStart: 0, 72 | inputEnd: shortcutText.length, 73 | shortcutText: shortcutText, 74 | prefix: "", 75 | suffix: "" 76 | }, expansionInfo); 77 | 78 | let foundMatch: boolean = false; 79 | 80 | // Handle any "???" 81 | const matches = [... shortcutText.matchAll(/\?\?\?/g) ]; 82 | let replacements = []; 83 | for (let i = 0; i < matches.length; i++) 84 | { 85 | const caption = parameterData?.[i]?.caption ?? "Parameter #" + (i+1); 86 | const value = parameterData?.[i]?.value || ""; 87 | const replacement = await Popups.getInstance().input(caption, value); 88 | if (replacement === null) 89 | { 90 | return null; 91 | } 92 | replacements.push(replacement); 93 | } 94 | for (let i = matches.length - 1; i >= 0; i--) 95 | { 96 | shortcutText = 97 | shortcutText.slice(0, matches[i].index) + 98 | replacements[i] + 99 | shortcutText.slice(matches[i].index + 3); 100 | } 101 | 102 | // Build an expansion script from the master list of shortcuts 103 | let expansionScript: string = ""; 104 | for (const shortcut of InlineScriptsPlugin.getInstance().shortcuts) 105 | { 106 | // Helper-blocker (an empty shortcut) just erases any helper scripts before it 107 | if ((!shortcut.test || shortcut.test.source === "(?:)") && !shortcut.expansion) 108 | { 109 | expansionScript = ""; 110 | continue; 111 | } 112 | 113 | // Does the shortcut fit the input text? (a helper script ALWAYS fits, since it's blank) 114 | const matchInfo: any = shortcutText.match(shortcut.test); 115 | if (!matchInfo) { continue; } 116 | 117 | // Translate any regex group results into variables/values for the expansion script 118 | for (let k: number = 1; k < matchInfo.length; k++) 119 | { 120 | expansionScript += 121 | "let $" + k + " = \"" + 122 | matchInfo[k].replaceAll("\\", "\\\\").replaceAll("\"", "\\\"") + "\";\n"; 123 | } 124 | 125 | // Add the shortcut's Expansion string to the Expanson script 126 | expansionScript += shortcut.expansion; 127 | 128 | // If this shortcut is not a helper script, stop checking for shortcut matches 129 | if (shortcut.test.source !== "(?:)") 130 | { 131 | foundMatch = true; 132 | break; 133 | } 134 | else 135 | { 136 | expansionScript += "\n"; 137 | } 138 | } 139 | 140 | let expansionText = null; 141 | if (foundMatch) 142 | { 143 | try 144 | { 145 | expansionText = await this.runExpansionScript( 146 | expansionScript, failSilently, expansionInfo); 147 | } 148 | catch (e: any) 149 | { 150 | if (!failSilently) { throw e; } 151 | } 152 | } 153 | expansionInfo.expansionText = expansionText; 154 | 155 | // If shortcut parsing amounted to nothing. Notify user of bad shortcut entry. 156 | if (expansionText === null) 157 | { 158 | if (!failSilently) 159 | { 160 | UserNotifier.run( 161 | { 162 | message: "Shortcut unidentified:\n\"" + shortcutText + "\"", 163 | messageLevel: "warn" 164 | }); 165 | } 166 | } 167 | 168 | // If there are any listeners for the onExpansion event, call them. If any of them return 169 | // true, then cancel the expansion. 170 | else if (expansionInfo.isUserTriggered && !expansionInfo.cancel && 171 | window._inlineScripts?.inlineScripts?.listeners?.onExpansion) 172 | { 173 | let replacementInput: string = null; 174 | HelperFncs.callEventListenerCollection( 175 | "inlineScripts.onExpansion", 176 | window._inlineScripts.inlineScripts.listeners.onExpansion, 177 | expansionInfo, 178 | (result: any) => 179 | { 180 | if (typeof result === "string") 181 | { 182 | replacementInput = result; 183 | } 184 | }); 185 | if (typeof replacementInput === "string") 186 | { 187 | return this.expand(replacementInput, false); 188 | } 189 | } 190 | 191 | if (expansionInfo.cancel) 192 | { 193 | expansionText = null; 194 | } 195 | 196 | return expansionText; 197 | } 198 | 199 | // Runs an expansion script, including error handling. 200 | // NOTE: Error handling is being done through window "error" event, rather than through 201 | // exceptions. This is because exceptions don't provide error line numbers whereas error 202 | // events do. Line numbers are important to create the useful "expansion failed" message. 203 | private static async runExpansionScript_internal 204 | (expansionScript: string, failSilently?: boolean, expansionInfo?: any): Promise 205 | { 206 | expansionInfo = expansionInfo || { isUserTriggered: false }; 207 | expansionInfo.cancel = false; 208 | 209 | expansionScript = AutoAsyncWrapper.run(expansionScript); 210 | 211 | // Run a pre-parser - finds the position of a parser error, if there is one 212 | let errorPosition = null; 213 | try 214 | { 215 | Parser.parse( 216 | "(async function(){\n" + expansionScript + "\n})", { ecmaVersion: 2021 }); 217 | } 218 | catch (e: any) 219 | { 220 | errorPosition = 221 | { 222 | line: e.loc.line - 1, 223 | column: e.loc.column + 1 224 | }; 225 | } 226 | 227 | // If we should fail silently, and we found an error, just quit 228 | if (failSilently && errorPosition) 229 | { 230 | throw null; 231 | } 232 | 233 | try 234 | { 235 | // Run the expansion script and return the result 236 | return await ( new AsyncFunction( 237 | "expand", "runExternal", "print", "expansionInfo", "popups", "expFormat", 238 | "expUnformat", 239 | expansionScript) ) 240 | ( this._boundExpand, ExternalRunner.run, UserNotifier.getFunction_print(), 241 | expansionInfo, Popups.getInstance(), HelperFncs.expFormat, HelperFncs.expUnformat 242 | ) ?? ""; 243 | } 244 | // If there was an error... 245 | catch (e: any) 246 | { 247 | // If we should fail silently, just quit 248 | if (failSilently) 249 | { 250 | throw null; 251 | } 252 | 253 | // If needed, get the error's position from the error object 254 | if (!errorPosition && e?.stack) 255 | { 256 | let match = e.stack.split("\n")[1].match(/([0-9]+):([0-9]+)/); 257 | if (match) 258 | { 259 | errorPosition = { line: Number(match[1])-2, column: Number(match[2]) }; 260 | } 261 | } 262 | 263 | // Display the error to the user, then quit 264 | this.handleExpansionError( 265 | expansionScript, e?.message || "Un-listed error", errorPosition, 266 | expansionInfo?.shortcutText); 267 | throw null; 268 | } 269 | } 270 | 271 | // Event handler for errors that occur during the expansion process 272 | private static handleExpansionError( 273 | expansionScript: string, message: string, position: any, shortcutText?: string): void 274 | { 275 | // Use spaces instead of tabs 276 | expansionScript = expansionScript.replaceAll("\t", " "); 277 | 278 | // Setup variables to be filled based on the given position 279 | let positionText = "\nline,column: ?,?"; 280 | let expansionText = ""; 281 | 282 | if (position) 283 | { 284 | // Get the expansion script, modified by line numbers and an arrow pointing to the error 285 | let expansionLines = expansionScript.split("\n"); 286 | 287 | // Add line numbers 288 | for (let i: number = 0; i < expansionLines.length; i++) 289 | { 290 | expansionLines[i] = String(i+1).padStart(4, "0") + " " + expansionLines[i]; 291 | } 292 | 293 | // Add arrows (pointing to error) 294 | expansionLines.splice(position.line, 0, "-".repeat(position.column + 4) + "^"); 295 | expansionLines.splice(position.line-1, 0, "-".repeat(position.column + 4) + "v"); 296 | 297 | // Fill message variables based on position 298 | positionText = "\nline,column: " + position.line + "," + position.column; 299 | expansionText = "\n" + "─".repeat(20) + "\n" + expansionLines.join("\n"); 300 | } 301 | 302 | const errorMessage = 303 | message + positionText + "\nshortcut-text: \"" + (shortcutText ?? "") + "\"" + 304 | expansionText; 305 | 306 | // Create a user message with the line and column of the error and the expansion script 307 | // showing where the error occurred. 308 | UserNotifier.run( 309 | { 310 | popupMessage: "Shortcut expansion issues.", 311 | consoleMessage: errorMessage, 312 | messageType: "SHORTCUT-EXPANSION-ERROR", 313 | consoleHasDetails: true 314 | }); 315 | 316 | // If there are any listeners for the onError event, call them 317 | if (window._inlineScripts?.inlineScripts?.listeners?.onError) 318 | { 319 | HelperFncs.callEventListenerCollection( 320 | "inlineScripts.onError", 321 | window._inlineScripts.inlineScripts.listeners.onError, errorMessage); 322 | } 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/ShortcutLinks.ts: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // Links - The ability to add links to a note that run expansions; either once or on each click. // 3 | /////////////////////////////////////////////////////////////////////////////////////////////////// 4 | 5 | "use strict"; 6 | 7 | import { MarkdownPostProcessorContext } from "obsidian"; 8 | import InlineScriptsPlugin from "./_Plugin"; 9 | import { ShortcutExpander } from "./ShortcutExpander"; 10 | import { HelperFncs } from "./HelperFncs"; 11 | 12 | export abstract class ShortcutLinks 13 | { 14 | public static staticConstructor(): void 15 | { 16 | this.staticConstructor_internal(); 17 | } 18 | 19 | /////////////////////////////////////////////////////////////////////////////////////////////////// 20 | 21 | private static staticConstructor_internal(): void 22 | { 23 | InlineScriptsPlugin.getInstance().registerMarkdownPostProcessor(this.processor); 24 | } 25 | 26 | private static processor(el: HTMLElement, ctx: MarkdownPostProcessorContext): void 27 | { 28 | let nodeList = el.querySelectorAll("code"); 29 | if (!nodeList.length) { return; } 30 | 31 | for (let index = 0; index < nodeList.length; index++) 32 | { 33 | const node = nodeList.item(index); 34 | const nodeInnerText = node.innerText; 35 | 36 | const notePath = ctx.sourcePath; 37 | 38 | // Block id target 39 | const target: string = 40 | (nodeInnerText.indexOf("^")==-1) ? null : 41 | nodeInnerText.slice(nodeInnerText.indexOf("^") + 1, nodeInnerText.indexOf(":")) 42 | .trim(); 43 | 44 | // Function for resolution 45 | const resolutionFnc: Function = 46 | nodeInnerText.match("^iscript-once(?: |:)") ? ShortcutLinks.linkResolution_once : 47 | nodeInnerText.match("^iscript-append(?: |:)") ? ShortcutLinks.linkResolution_append : 48 | nodeInnerText.match("^iscript-prepend(?: |:)") ? ShortcutLinks.linkResolution_prepend : 49 | nodeInnerText.match("^iscript(?: |:)") ? ShortcutLinks.linkResolution_standard : 50 | null; 51 | if (!resolutionFnc) 52 | { 53 | continue; 54 | } 55 | 56 | // Split the iscript link data section into trimmed parts 57 | let parts = nodeInnerText.slice(nodeInnerText.indexOf(":") + 1).split(/ ?\| ?/g); 58 | if (parts[0] === "") { continue; } 59 | let shortcutText = parts[0]; 60 | parts = parts.map(v => v.trim()); 61 | 62 | // Remove optional extra spaces on either side the shortcut text (DON'T trim as only up 63 | // to one space on either side should be removed) 64 | if (shortcutText.startsWith(" ")) { shortcutText = shortcutText.slice(1); } 65 | if (shortcutText.endsWith(" ")) { shortcutText = shortcutText.slice(0, -1); } 66 | 67 | // New "a" element 68 | let a = document.createElement("a"); 69 | a.classList.add("internal-link"); 70 | a.classList.add("iscript-link"); 71 | a.dataset["source"] = nodeInnerText; 72 | a.innerText = parts[1] || parts[0]; 73 | a.setAttr("href", "#"); 74 | 75 | // Click response 76 | a.onclick = async function() 77 | { 78 | let targetPos = null; 79 | 80 | // If a target is provided, get the target's place in the note. 81 | if (target) 82 | { 83 | const noteFile = (app.vault as any).fileMap[notePath]; 84 | if (!noteFile) { return; } 85 | const fileCache = app.metadataCache.getFileCache(noteFile); 86 | if (!fileCache) { return; } 87 | const blockData = fileCache.blocks[target]; 88 | if (!blockData) { return; } 89 | targetPos = 90 | { 91 | start: blockData.position.start.offset, 92 | end: blockData.position.end.offset 93 | }; 94 | // Don't overwrite the block id 95 | const noteContent = 96 | await app.vault.cachedRead((app.vault as any).fileMap[notePath]); 97 | const blockContent = noteContent.slice(targetPos.start, targetPos.end); 98 | const idMatch = blockContent.match(/\s\^[^\n]+$/); 99 | if (idMatch) 100 | { 101 | targetPos.end -= idMatch[0].length; 102 | } 103 | } 104 | // Expand the iscript shortcut 105 | let result = await ShortcutExpander.expand( 106 | shortcutText, false, { isUserTriggered: true }, 107 | parts.slice(3).map(v => { return { caption: v }; }) 108 | ); 109 | 110 | // If iscript shortcut expanded, run output customization, then resolution 111 | if (result) 112 | { 113 | // Customizing entry handling 114 | if (parts.length > 2 && parts[2]) 115 | { 116 | result = (new Function('$$', "return " + parts[2]))(result); 117 | } 118 | // Resolve result 119 | resolutionFnc(this, result, targetPos); 120 | } 121 | }; 122 | 123 | // Replace code node with "a" element 124 | node.parentNode.insertBefore(a, node); 125 | node.remove(); 126 | } 127 | } 128 | 129 | private static linkResolution_standard(ui: HTMLElement, expansion: string, targetPos: any) 130 | { 131 | if (targetPos) 132 | { 133 | HelperFncs.addToNote(expansion, targetPos); 134 | } 135 | else 136 | { 137 | ui.innerHTML = expansion; 138 | } 139 | } 140 | 141 | private static linkResolution_once(ui: HTMLElement, expansion: string, targetPos: any) 142 | { 143 | let newUi: HTMLElement = document.createElement("span"); 144 | if (targetPos) 145 | { 146 | newUi.innerHTML = ui.innerHTML; 147 | HelperFncs.addToNote(expansion, targetPos); 148 | } 149 | else 150 | { 151 | newUi.innerHTML = expansion; 152 | } 153 | ui.parentNode.insertBefore(newUi, ui); 154 | ui.remove(); 155 | } 156 | 157 | private static linkResolution_append(ui: HTMLElement, expansion: string, targetPos: any) 158 | { 159 | if (targetPos) { targetPos.start = targetPos.end; } 160 | HelperFncs.addToNote(expansion, targetPos); 161 | } 162 | private static linkResolution_prepend(ui: HTMLElement, expansion: string, targetPos: any) 163 | { 164 | if (targetPos) 165 | { 166 | targetPos.end = targetPos.start; 167 | } 168 | else 169 | { 170 | targetPos = { start: 0, end: 0 }; 171 | } 172 | HelperFncs.addToNote(expansion, targetPos); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/ShortcutLoader.ts: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // Shortcut loader - Load shortcuts from a shortcut-file, or from all shortcut-files & settings. // 3 | /////////////////////////////////////////////////////////////////////////////////////////////////// 4 | 5 | "use strict"; 6 | 7 | import InlineScriptsPlugin from "./_Plugin"; 8 | import { UserNotifier } from "./ui_userNotifier"; 9 | import { ShortcutExpander } from "./ShortcutExpander"; 10 | import { InputBlocker } from "./ui_InputBlocker"; 11 | import { ButtonView } from "./ui_ButtonView"; 12 | import { HelperFncs } from "./HelperFncs"; 13 | 14 | const REGEX_NOTE_METADATA: RegExp = /^\n*---\n(?:[^-]+\n)?---\n/; 15 | const REGEX_SPLIT_FIRST_DASH: RegExp = / - (.*)/s; 16 | const REGEX_SFILE_SECTION_SPLIT: RegExp = /^__$/gm; 17 | const REGEX_ALTERNATIVE_SYNTAX: RegExp = /\n\t- Alternative: __([^_]+)__/; 18 | const ESCAPED_CHARACTERS: Set = new Set( 19 | [ 20 | ".", "+", "*", "?", "[", "^", "]", "$", "(", ")", "{", "}", "=", "!", "<", ">", "|", ":", "-", 21 | "\\", "\"", "'", "`" 22 | ]); 23 | const GENERAL_HELP_PREAMBLE = `return [ "#### Help - General 24 | Here are shortcuts for help with __Inline Scripts__. 25 | - __help__ - Shows this text. 26 | - __ref settings__ - Describes shortcuts defined in the Settings. 27 | - __ref all__ - Describes _all_ shortcuts (except for the ones in this list).`; 28 | const GENERAL_HELP_PREAMBLE_SHORTCUT_FILES = ` 29 | - For help on specific shortcut-files, __help__ and __ref__ can be followed by:`; 30 | const SFILE_HELP_PREAMBLE = `return "#### Help - $1 31 | _Use shortcut __ref $2__ for a list of shortcuts._ 32 | 33 | `; 34 | const SFILE_REF_PREAMBLE = `let result = "#### Reference - $1 35 | _Use shortcut __help $2__ for general help._ 36 | ";`; 37 | const SORT_SYNTAXES = (a: any, b: any): number => 38 | { 39 | if (a.text === "help") { return -1; } 40 | else if (b.text === "help") { return 1; } 41 | else 42 | { 43 | const lhs = a.text 44 | .replaceAll("{", "0") 45 | .replaceAll(/(^|[^\\])~/g, "$1" + String.fromCharCode(9)); 46 | const rhs = b.text 47 | .replaceAll("{", "0") 48 | .replaceAll(/(^|[^\\])~/g, "$1" + String.fromCharCode(9)); 49 | return lhs.localeCompare(rhs); 50 | } 51 | } 52 | 53 | export abstract class ShortcutLoader 54 | { 55 | // Parses a shortcut-file's contents into a useful data format and returns it 56 | public static parseShortcutFile( 57 | filename: string, content: string, maintainCodeFence?: boolean, 58 | maintainAboutString?: boolean) : any 59 | { 60 | return this.parseShortcutFile_internal( 61 | filename, content, maintainCodeFence, maintainAboutString); 62 | } 63 | 64 | // Offer "setupShortcuts" function for use as a callback. 65 | // Function loads all shortcuts from the settings and shortcut-file list into the plugin. 66 | public static getFunction_setupShortcuts(): Function 67 | { 68 | return this.setupShortcuts_internal.bind(this); 69 | } 70 | 71 | /////////////////////////////////////////////////////////////////////////////////////////////////// 72 | 73 | private static isSettingUpShortcuts: boolean = false; 74 | 75 | private static parseShortcutFile_internal( 76 | filename: string, content: string, maintainCodeFence?: boolean, 77 | maintainAboutString?: boolean) : any 78 | { 79 | // Sanitize newlines. "\r" disrupts calculations, including the regex replace. 80 | content = content.replaceAll("\r", ""); 81 | 82 | // Remove any note metadata 83 | content = content.replace(REGEX_NOTE_METADATA, ""); 84 | 85 | // Result vars 86 | let fileAbout: string = ""; 87 | let shortcuts: Array = []; 88 | let shortcutAbouts: Array = []; 89 | 90 | // Flag set when an error occurs. Used for single popup for ALL file errors. 91 | let fileHasErrors: boolean = false; 92 | 93 | // Get sections from file contents 94 | const sections: Array = 95 | content.split(REGEX_SFILE_SECTION_SPLIT).map((v: string) => v.trim()); 96 | fileAbout = sections[0]; 97 | 98 | // Check for the obvious error of misnumbered sections (bounded by "__") 99 | if (sections.length === 1) 100 | { 101 | UserNotifier.run( 102 | { 103 | message: 104 | "Shortcut-file \"" + filename + "\"\nhas no shortcuts." + 105 | "\n\n(Shortcut-files are sectioned with \"__\")", 106 | messageLevel: "warn", 107 | }); 108 | } 109 | else if ((sections.length-1) % 3) 110 | { 111 | UserNotifier.run( 112 | { 113 | consoleMessage: "In shortcut-file \"" + filename + "\"", 114 | messageType: "MISNUMBERED-SECTION-COUNT-ERROR" 115 | }); 116 | fileHasErrors = true; 117 | } 118 | 119 | // Parse each shortcut in the file 120 | // NOTE: this loop checks i+2 and increments by 3 as it uses i, i+1 and i+2. 121 | for (let i: number = 1; i+2 < sections.length; i += 3) 122 | { 123 | // Test string handling 124 | let testRegex: any = null; 125 | if (maintainCodeFence) 126 | { 127 | // "maintainCodeFence" is not possible with a real RegExp object. 128 | // Instead, create RegExp-style-dummy to retain fence within API. 129 | testRegex = { source: sections[i] }; 130 | } 131 | else 132 | { 133 | let c = sections[i]; 134 | 135 | // Handle the Test being in a basic fenced code-block 136 | if (c.startsWith("```") && c.endsWith("```")) 137 | { 138 | c = c.slice(3, -3).trim(); 139 | } 140 | 141 | try 142 | { 143 | testRegex = new RegExp(c, "i"); 144 | } 145 | catch (e: any) 146 | { 147 | UserNotifier.run( 148 | { 149 | consoleMessage: "In shortcut-file \"" + filename + "\":\n" + c, 150 | messageType: "BAD-TEST-STRING-ERROR" 151 | }); 152 | fileHasErrors = true; 153 | continue; 154 | } 155 | } 156 | 157 | // Expansion string handling 158 | let exp: string = sections[i+1]; 159 | // Handle the Expansion being in a JavaScript fenced code-block 160 | if (!maintainCodeFence) 161 | { 162 | if (exp.startsWith("```js") && exp.endsWith("```")) 163 | { 164 | exp = exp.slice(5, -3).trim(); 165 | } 166 | } 167 | 168 | // Add shortcut to result 169 | if (maintainAboutString) 170 | { 171 | shortcuts.push({ 172 | test: testRegex, expansion: exp, about: sections[i+2] }); 173 | } 174 | else 175 | { 176 | shortcuts.push({ test: testRegex, expansion: exp }); 177 | } 178 | 179 | // About string handling 180 | // Skip if it's a helper script, helper blocker or setup script, or if the About 181 | // string's syntax string is the string "hidden" 182 | if (testRegex.source !== "(?:)" && testRegex.source.toLowerCase() !== "^sfile setup$" && 183 | testRegex.source.toLowerCase() !== "^sfile shutdown$" && 184 | !sections[i+2].startsWith("hidden - ")) 185 | { 186 | let aboutParts: Array = 187 | sections[i+2].split(REGEX_SPLIT_FIRST_DASH).map((v: string) => v.trim()); 188 | // If no syntax string is included, use the Regex string instead 189 | if (aboutParts.length === 1) 190 | { 191 | aboutParts = [testRegex.source, aboutParts[0]]; 192 | } 193 | shortcutAbouts.push({ syntax: aboutParts[0], description: aboutParts[1] }); 194 | } 195 | } 196 | 197 | // If errors during parsing, notify user in a general popup notification. 198 | // Any errors were added to console at the moment they were found. 199 | if (fileHasErrors) 200 | { 201 | UserNotifier.run( 202 | { 203 | popupMessage: "Shortcut-file issues\n" + filename, 204 | consoleHasDetails: true 205 | }); 206 | } 207 | 208 | // Return result of parsing the shortcut file 209 | return { shortcuts: shortcuts, fileAbout: fileAbout, shortcutAbouts: shortcutAbouts }; 210 | } 211 | 212 | private static async setupShortcuts_internal(): Promise 213 | { 214 | // Prevent running this function more than once at a time 215 | if (this.isSettingUpShortcuts) { return; } 216 | this.isSettingUpShortcuts = true; 217 | 218 | const plugin = InlineScriptsPlugin.getInstance(); 219 | 220 | // To fill with data for the generation of help shortcuts 221 | let abouts: Array = []; 222 | 223 | // Restart the master list of shortcuts 224 | plugin.shortcuts = [ { test: /^help ?$/, expansion: "" } ]; 225 | let shortcutFiles: Array = []; 226 | this.updateGeneralHelpShortcut(shortcutFiles); 227 | 228 | // Restart the master list of shortcut syntaxes 229 | plugin.syntaxes = []; 230 | 231 | // Add shortcuts defined directly in the settings 232 | let parseResult: any = 233 | this.parseShortcutFile("settings", plugin.settings.shortcuts); 234 | plugin.shortcuts = plugin.shortcuts.concat(parseResult.shortcuts); 235 | abouts.push({ filename: "", shortcutAbouts: parseResult.shortcutAbouts }); 236 | 237 | // Add shortcut syntaxes to the master list 238 | this.addShortcutFileSyntaxes( 239 | "settings", 240 | parseResult.shortcutAbouts, 241 | [ [ "help", "A list of helpful shortcuts" ], 242 | [ "ref settings", "A list of shortcuts defined in settings" ], 243 | [ "ref all", "A list of ALL shortcuts" ] ]); 244 | 245 | // Add a helper-blocker to segment helper scripts within their shortcut-files 246 | plugin.shortcuts.push({}); 247 | 248 | // Setup a list of indices for shortcut-files, available to shortcuts 249 | HelperFncs.confirmObjectPath("_inlineScripts.inlineScripts"); 250 | window._inlineScripts.inlineScripts.sfileIndices = {}; 251 | let sfileIndicesIndex = 0; 252 | 253 | // Go over all shortcut-files 254 | for (const shortcutFile of plugin.settings.shortcutFiles) 255 | { 256 | if (!shortcutFile.enabled) { continue; } 257 | const file: any = (plugin.app.vault as any).fileMap[shortcutFile.address]; 258 | if (!file) 259 | { 260 | UserNotifier.run( 261 | { 262 | popupMessage: "Missing shortcut-file\n" + shortcutFile.address, 263 | consoleMessage: shortcutFile.address, 264 | messageType: "MISSING-SHORTCUT-FILE-ERROR" 265 | }); 266 | continue; 267 | } 268 | 269 | const content: string = await plugin.app.vault.cachedRead(file); 270 | 271 | // Parse shortcut-file contents 272 | parseResult = this.parseShortcutFile(shortcutFile.address, content) 273 | 274 | // Look for a "setup" script in this shortcut-file. Run if found. 275 | const setupScript = this.getExpansionScript("^sfile setup$", parseResult.shortcuts); 276 | if (setupScript) 277 | { 278 | // Disable input while running the setup script (in case it takes a while) 279 | InputBlocker.setEnabled(true); 280 | 281 | // Run the setup script 282 | try 283 | { 284 | // If setup script returns TRUE, don't use shortcuts in this shortcut-file 285 | if (await ShortcutExpander.runExpansionScript( 286 | setupScript, false, { shortcutText: "sfile setup" })) 287 | { 288 | parseResult.shortcuts = null; 289 | } 290 | } 291 | catch (e: any) 292 | { 293 | // If setup script failed, don't use the shortcuts in this shortcut-file 294 | parseResult.shortcuts = null; 295 | } 296 | 297 | // Enable input, now that the expansion is over 298 | InputBlocker.setEnabled(false); 299 | } 300 | 301 | // If setup script returned true, abort adding the new shortcuts 302 | if (!parseResult.shortcuts) { continue; } 303 | 304 | // Look for "shutdown" script in this shortcut-file. Store if found. 305 | const shutdownScript = 306 | this.getExpansionScript("^sfile shutdown$", parseResult.shortcuts); 307 | if (shutdownScript) 308 | { 309 | plugin.shutdownScripts[shortcutFile.address] = shutdownScript; 310 | } 311 | 312 | // Add new shortcuts to master list, followed by helper-blocker 313 | plugin.shortcuts = plugin.shortcuts.concat(parseResult.shortcuts); 314 | plugin.shortcuts.push({}); 315 | 316 | // Get the file About string and shortcut About strings 317 | let baseName: string = 318 | shortcutFile.address.slice(shortcutFile.address.lastIndexOf("/")+1, -3); 319 | baseName = 320 | baseName.endsWith(".sfile") ? 321 | baseName.slice(0, -6) : 322 | baseName; 323 | shortcutFiles.push(baseName); 324 | this.updateGeneralHelpShortcut(shortcutFiles); 325 | abouts.push( 326 | { 327 | filename: baseName, 328 | fileAbout: parseResult.fileAbout, 329 | shortcutAbouts: parseResult.shortcutAbouts 330 | }); 331 | 332 | // Add shortcut syntaxes to the master list 333 | this.addShortcutFileSyntaxes( 334 | baseName, 335 | parseResult.shortcutAbouts, 336 | [ [ "help " + baseName, 337 | "Description of the \"" + baseName + "\" shortcut-file." ], 338 | [ "ref " + baseName, 339 | "A list of shortcuts defined in the \"" + baseName + "\" shortcut-file." ] ]); 340 | 341 | window._inlineScripts.inlineScripts.sfileIndices[baseName] = sfileIndicesIndex; 342 | sfileIndicesIndex++; 343 | } 344 | 345 | // Generate and add help shortcuts 346 | plugin.shortcuts = this.generateHelpShortcuts(abouts).concat(plugin.shortcuts); 347 | 348 | // Finalize the master syntaxes list 349 | this.finalizeShortcutSyntaxes(); 350 | 351 | // ButtonView needs to be updated with the latest shortcut info 352 | ButtonView.getInstance()?.refreshGroupUi(); 353 | 354 | // Call any listeners of the shortcutsLoaded event 355 | if (window._inlineScripts?.inlineScripts?.listeners?.onShortcutsLoaded) 356 | { 357 | HelperFncs.callEventListenerCollection( 358 | "inlineScripts.onShortcutsLoaded", 359 | window._inlineScripts.inlineScripts.listeners.onShortcutsLoaded); 360 | } 361 | 362 | // End the block on other calls to this function 363 | this.isSettingUpShortcuts = false; 364 | } 365 | 366 | private static getExpansionScript(scriptId: string, shortcuts: Array): string 367 | { 368 | let result = ""; 369 | for (const shortcut of shortcuts) 370 | { 371 | if (!shortcut.test.source || shortcut.test.source === "(?:)") 372 | { 373 | if (!shortcut.expansion) { result = ""; } 374 | else 375 | { 376 | result += shortcut.expansion; 377 | } 378 | } 379 | else if (shortcut.test.source.toLowerCase() === scriptId) 380 | { 381 | result += shortcut.expansion; 382 | break; 383 | } 384 | } 385 | return result; 386 | } 387 | 388 | private static updateGeneralHelpShortcut(shortcutFiles: Array): void 389 | { 390 | let expansion = GENERAL_HELP_PREAMBLE.replaceAll("\n", "\\n"); 391 | if (shortcutFiles.length > 0) 392 | { 393 | expansion += 394 | GENERAL_HELP_PREAMBLE_SHORTCUT_FILES.replaceAll("\n", "\\n") + "\", " + 395 | "\"\\n - " + shortcutFiles.join("\",\"\\n - "); 396 | } 397 | expansion += "\", \"\\n\\n\" ];" 398 | InlineScriptsPlugin.getInstance().shortcuts[0].expansion = expansion; 399 | } 400 | 401 | // Creates help shortcuts based on "about" info from shortcuts and shortcut-files 402 | private static generateHelpShortcuts(abouts: any): Array 403 | { 404 | // The final list of help shortcuts 405 | let result: Array = []; 406 | 407 | // Helper functions 408 | function capitalize(s: string) 409 | { 410 | return s.charAt(0).toUpperCase() + s.slice(1); 411 | } 412 | function stringifyString(s: string) 413 | { 414 | return s.replaceAll("\"", "\\\"").replaceAll("\n", "\\n"); 415 | } 416 | function makeHelpShortcut(name: string, about: string) 417 | { 418 | about ||= "No information available."; 419 | const expansion: string = 420 | SFILE_HELP_PREAMBLE.replaceAll("\n", "\\n").replaceAll("$1", capitalize(name)). 421 | replaceAll("$2", name) + 422 | stringifyString(about) + 423 | "\\n\\n\";"; 424 | const test: RegExp = new RegExp("^help " + name + "$"); 425 | result.push({ test: test, expansion: expansion }); 426 | } 427 | function makeRefShortcut( 428 | groupName: string, abouts: any, displayName?: string, removeHr?: boolean) 429 | { 430 | displayName = displayName || capitalize(groupName); 431 | let expansion: string = 432 | SFILE_REF_PREAMBLE.replaceAll("\n", "\\n").replaceAll("$1", displayName). 433 | replaceAll("$2", groupName) + "\n"; 434 | for (const about of abouts) 435 | { 436 | let description: string = ""; 437 | if (about.description) 438 | { 439 | description = " - " + stringifyString(about.description); 440 | if (removeHr) { description = description.replaceAll("\\n***", ""); } 441 | } 442 | expansion += 443 | "result += \"- __" + stringifyString(about.syntax) + "__" + 444 | description + "\\n\";\n"; 445 | } 446 | if (!abouts.length) 447 | { 448 | expansion += "result += \"\\nNo shortcuts\\n\";\n"; 449 | } 450 | expansion += "return result + \"\\n\";"; 451 | const test: RegExp = new RegExp("^ref(?:erence)? " + groupName + "$"); 452 | result.push({ test: test, expansion: expansion }); 453 | } 454 | 455 | // Gather info 456 | let settingsAbouts: Array = []; 457 | let shortcutFileAbouts: Array = []; 458 | for (const about of abouts) 459 | { 460 | // If not the "settings" shortcut-file (the only about with a blank filename) 461 | if (about.filename) 462 | { 463 | // Add help only for shortcut-files that contain non-hidden shortcuts 464 | if (about.shortcutAbouts.length === 0) { continue; } 465 | // Make "help" shortcut for this shortcut-file 466 | makeHelpShortcut(about.filename, about.fileAbout); 467 | // Make "ref" shortcut for this shortcut-file 468 | makeRefShortcut(about.filename, about.shortcutAbouts); 469 | // Add to "ref all" list: reference of ALL shortcuts 470 | shortcutFileAbouts = shortcutFileAbouts.concat(about.shortcutAbouts); 471 | } 472 | else if (about.shortcutAbouts.length > 0) 473 | { 474 | // Add to "ref all" list: reference of ALL shortcuts 475 | settingsAbouts = about.shortcutAbouts; 476 | } 477 | } 478 | 479 | // Create "ref all" shortcut: expands to a reference for ALL shortcuts 480 | makeRefShortcut( 481 | "?(?:all)?", settingsAbouts.concat(shortcutFileAbouts), "All shortcuts", true); 482 | 483 | // Create "ref settings" shortcut: expands to a reference for shortcuts defined in settings 484 | makeRefShortcut("settings", settingsAbouts); 485 | 486 | // Reversing ensures that "ref all" and "ref settings" aren't superseded by a poorly named 487 | // shortcut-file 488 | result.reverse(); 489 | 490 | // Return list of help shortcuts we just generated 491 | return result; 492 | } 493 | 494 | private static addShortcutFileSyntaxes(sfile: string, abouts: Array, syntaxes: Array) 495 | { 496 | const plugin = InlineScriptsPlugin.getInstance(); 497 | 498 | const addSyntax = (syntax: string, description: string, about: any, sfile: string) => 499 | { 500 | description = description.replaceAll("\n***", ""); 501 | plugin.syntaxes.push({ text: syntax, description: description, sfile: sfile }); 502 | 503 | if (about) { about.syntax = this.removeSyntaxSpecialCharacters(syntax); } 504 | 505 | const altSyntax: string = description.match(REGEX_ALTERNATIVE_SYNTAX)?.[1]; 506 | if (altSyntax) 507 | { 508 | const altDescription = description.replace( 509 | REGEX_ALTERNATIVE_SYNTAX, "\n\t- Alternative: __" + syntax + "__"); 510 | plugin.syntaxes.push({ text: altSyntax, description: altDescription, sfile: "" }); 511 | } 512 | } 513 | 514 | for (const about of abouts) 515 | { 516 | addSyntax(about.syntax, about.description, about, sfile); 517 | } 518 | for (const syntax of syntaxes) 519 | { 520 | addSyntax(syntax[0], syntax[1], null, ""); 521 | } 522 | } 523 | 524 | private static finalizeShortcutSyntaxes(): void 525 | { 526 | const plugin = InlineScriptsPlugin.getInstance(); 527 | 528 | plugin.syntaxesSorted = [... plugin.syntaxes ]; 529 | plugin.syntaxesSorted.sort(SORT_SYNTAXES); 530 | 531 | for (let syntax of plugin.syntaxes) 532 | { 533 | syntax.text = this.removeSyntaxSpecialCharacters(syntax.text); 534 | syntax.regex = this.generateSyntaxRegex(syntax.text); 535 | } 536 | } 537 | 538 | private static removeSyntaxSpecialCharacters(src: string): string 539 | { 540 | return src.replaceAll(/(^|[^\\])~/g, "$1").replaceAll(/\\(?=-|~)/g, ""); 541 | } 542 | 543 | private static generateSyntaxRegex(syntax: string): RegExp 544 | { 545 | let result: string = "^"; 546 | for (let i = 0; i < syntax.length; i++) 547 | { 548 | if (syntax[i] === "{") 549 | { 550 | const parameterEnd = syntax.indexOf("}", i); 551 | const parameterSyntax = syntax.slice(i, parameterEnd + 1); 552 | 553 | // Find details about the parameter 554 | // NOTE - the "\u241F" is a dummy character used in autocomplete to check if the 555 | // carat is in a parameter. Numbers need to include it in order to pass that check. 556 | const expectRegex = 557 | (parameterSyntax.match(/: >0(?:}|,)/)) ? "[0-9|\u241F]%1" : // How to do "[1-9][0-9]%1"? 558 | (parameterSyntax.match(/: >=0(?:}|,)/)) ? "[0-9|\u241F]%1" : 559 | (parameterSyntax.match(/: path text(?:}|,)/)) ? "\"[^\"]%1|[^ ]%1" : 560 | (parameterSyntax.match(/: text(?:}|,| \()/)) ? ".%1" : 561 | "[^ ]%1"; 562 | const defaultRegex = (parameterSyntax.includes(", default:")) ? "*" : "+"; 563 | 564 | // Build a regex part based on the details of the parameter 565 | result += "(?:(" + expectRegex.replaceAll("%1", defaultRegex) + ")|$)"; 566 | 567 | // Bump character checking past the parameter 568 | i = parameterEnd; 569 | } 570 | else 571 | { 572 | result += "(?:" + this.escapeCharacterForRegex(syntax[i]) + "|$)"; 573 | } 574 | } 575 | result += "$"; 576 | return new RegExp(result); 577 | } 578 | 579 | private static escapeCharacterForRegex(src: string): string 580 | { 581 | return (!ESCAPED_CHARACTERS.has(src)) ? src : ("\\" + src); 582 | } 583 | } 584 | -------------------------------------------------------------------------------- /src/_.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare global 4 | { 5 | interface Window 6 | { 7 | _inlineScripts: any; 8 | request: Function; 9 | plugin: any; 10 | dbg: boolean; 11 | brk: Function; 12 | getEmPixels: Function; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/_Plugin.ts: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////// 2 | // plugin - Class containing the main logic for this plugin project // 3 | ////////////////////////////////////////////////////////////////////// 4 | 5 | "use strict"; 6 | 7 | import { Plugin, MarkdownView } from "obsidian"; 8 | import { UserNotifier } from "./ui_userNotifier"; 9 | import { InlineScriptsPluginSettings } from "./ui_settings"; 10 | import { DEFAULT_SETTINGS } from "./defaultSettings"; 11 | import { Dfc, DfcMonitorType } from "./Dfc"; 12 | import { ShortcutExpander } from "./ShortcutExpander"; 13 | import { ShortcutLoader } from "./ShortcutLoader"; 14 | import { ShortcutLinks } from "./ShortcutLinks"; 15 | import { AutoComplete } from "./AutoComplete"; 16 | import { InputBlocker } from "./ui_InputBlocker"; 17 | import { Popups } from "./ui_Popups"; 18 | import { ButtonView } from "./ui_ButtonView"; 19 | import { HelperFncs } from "./HelperFncs"; 20 | 21 | // NOTE: The "Inline Scripts" plugin uses a custom format for shortcut-files. I tried using 22 | // existing formats (json, xml, etc), but they were cumbersome for developing JavaScript code in. 23 | // The chosen format is simple, flexible, and allows for wrapping scripts in js-fenced-code-blocks. 24 | // This makes it easy to write Expansion scripts within Obsidian which is the intended use-case. 25 | // For a summary of the format, see here: 26 | // https://github.com/jon-heard/obsidian-inline-scripts#tutorial-create-a-new-shortcut-file 27 | // and here: 28 | // https://github.com/jon-heard/obsidian-inline-scripts#development-aid-fenced-code-blocks 29 | 30 | const ANNOUNCEMENTS: Array = 31 | [ 32 | { 33 | version: "0.21.0", 34 | message: 35 | "0.21.x is a major release for open-beta phase.\nIt has some great features! " + 36 | "However...\n A few of the changes may be incompatible with existing shortcuts " + 37 | "and/or shortcut-files.\n" + 38 | "" + 39 | "Please check here for details\n...including some simple steps to resolve any " + 40 | "incompatibilities." 41 | }, 42 | { 43 | version: "0.22.0", 44 | message: 45 | "0.22.x adds some notable features:
    " + 46 | "
  • A side panel onto which you can add custom buttons to quickly run shortcuts.
  • " + 47 | "
  • Links you can add to your notes that will run a shortcut when clicked.
  • " + 48 | "
  • Tutorial videos for the more stable and complex shortcut-files in the library
  • " + 49 | "
  • Support shortcut-files added to the library (X_ui.sfile) to provide more graphical interfaces.
  • " + 50 | "
Watch this video for " + 51 | "a demonstration of the major features being added in 0.22.0.\n" + 52 | "Check " + 53 | "here for more details on this release." 54 | }, 55 | { 56 | version: "0.23.0", 57 | message: 58 | "
NOTE - The old Inline Scripts library " + 59 | "is incompatible with this new release. Re-import the latest library to resolve " + 60 | "the errors.

" + 61 | "0.23.x adds some quality of life features. Notably:
    " + 62 | "
  • The state system now automatically maintains state between Obsidian sessions." + 63 | "
    No need to manually save and restore session state.
  • " + 64 | "
  • Various features added to the cards system.
" + 65 | "Check " + 66 | "the release notes for details.
" 67 | }, 68 | { 69 | version: "0.23.2", 70 | message: 71 | "
The save-on-quit feature that was added in 0.23.0 " + 72 | "works... most of the time.

It turns out that \"quit\" scripts aren't " + 73 | "guaranteed! This update adds auto-save to mitigate data loss if save-on-quit " + 74 | "fails.

" + 75 | "NOTE - This release updates the library. Make sure you import the latest library!
" 76 | }, 77 | { 78 | version: "0.24.0", 79 | message: 80 | "
" + 81 | "Major plugin updates" + 82 | "
    " + 83 | "
  • a standard format for expansion strings. This includes:" + 84 | "
      " + 85 | "
    • settings (prefix, line-prefix, suffix) for format customization
    • " + 86 | "
    • expFormat() - converts a string into the standard format
    • " + 87 | "
    • expUnformat() - removes the standard format from a string
    • " + 88 | "
    " + 89 | "
  • Shortcut links can include a block-id for the shortcut expansion destination
  • " + 90 | "
  • Autocomplete and/or its tooltip can be disabled in the settings
  • " + 91 | "
" + 92 | "Major library updates" + 93 | "
    " + 94 | "
  • Cards - This system has been revamped. The shortcut \"help cards\" provides a link to a tutorial video.
  • " + 95 | "
  • Tablefiles - A new system to roll on tables in text files. The shortcut \"help tablefiles\" provides a link to a tutorial video.
  • " + 96 | "
  • State - The state is auto-saved to a file beside the state shortcut-file. Auto-save is now more reliable.
  • " + 97 | "
  • Notevars - Incompatibily with the latest Obsidian is resolved. A bug where multiple \"set\" shortcuts only save one of the variables is resolved.
  • " + 98 | "
" + 99 | "Check the " + 100 | "release notes for details." + 101 | "
" 102 | }, 103 | { 104 | version: "0.24.11", 105 | message: 106 | "
" + 107 | " Summary of notable changes since 0.24.0" + 108 | "
    " + 109 | "
  • tablefiles
      " + 110 | "
    • Popup - can change multiple table configurations simultaneously by selecting multiple tables with shift or ctrl/cmd keys
    • " + 111 | "
    • Table files can now include a YAML frontmatter which can define their configuration. This can be edited from the popup, just like other configurations.
    • " + 112 | "
    • A tbl reroll shortcut has been added to allow re-rolling the prior table roll.
    • " + 113 | "
    • Folder paths are now added recursively (table-files in the folder AND subfolder are added)
    • " + 114 | "
  • " + 115 | "
  • cards
      " + 116 | "
    • draw and pick shortcuts now have a variation that allows entering the from, to and count parameters in any order.
    • " + 117 | "
  • " + 118 | "
  • plugin
      " + 119 | "
    • Settings shows alerts when there are updates available for the plugin and/or library
    • " + 120 | "
    • README - Added documentation for the useful unblock() function and a reference for all helper functions
    • " + 121 | "
  • " + 122 | "
" + 123 | "
" 124 | } 125 | ]; 126 | 127 | export default class InlineScriptsPlugin extends Plugin 128 | { 129 | // Store the plugin's settings 130 | public settings: any; 131 | // Keep track of the suffix's final character 132 | public suffixEndCharacter: string; 133 | // Keep track of shutdown scripts for any shortcut-files that have them 134 | public shutdownScripts: any = {}; 135 | // Keep a Dfc for shortcut-files. This lets us monitor changes to them. 136 | public shortcutDfc: Dfc; 137 | // The master list of shortcuts: all registered shortcuts. Referenced during expansion. 138 | public shortcuts: Array; 139 | // The instance of the settings panel UI 140 | public settingsUi: InlineScriptsPluginSettings; 141 | // The master list of shortcut syntaxes (provided by the About strings of all shortcuts) 142 | public syntaxes: Array; 143 | // The same thing as "syntaxes", but sorted by syntax. 144 | public syntaxesSorted: Array; 145 | // If set, all keyboard input is ignored 146 | public inputDisabled: boolean; 147 | 148 | public onload(): void 149 | { 150 | this.onload_internal(); 151 | } 152 | 153 | public onunload(): void 154 | { 155 | this.onunload_internal(); 156 | } 157 | 158 | public saveSettings(): void 159 | { 160 | this.saveData(this.settings); 161 | } 162 | 163 | // Returns an array of the addresses for all shortcut-files that are registered and enabled 164 | public getActiveShortcutFileAddresses(): Array 165 | { 166 | return this.settings.shortcutFiles.filter((f: any) => f.enabled).map((f: any) => f.address); 167 | } 168 | 169 | public static getInstance(): InlineScriptsPlugin 170 | { 171 | return this._instance; 172 | } 173 | 174 | public static getDefaultSettings(): any 175 | { 176 | return Object.assign({}, DEFAULT_SETTINGS); 177 | } 178 | 179 | public tryShortcutExpansion(): void 180 | { 181 | this.tryShortcutExpansion_internal(); 182 | } 183 | 184 | /////////////////////////////////////////////////////////////////////////////////////////////////// 185 | 186 | private static _instance: InlineScriptsPlugin; 187 | 188 | private _cm5_handleExpansionTrigger: any; 189 | private _runAllShutdownScripts: any; 190 | private _autocomplete: AutoComplete; 191 | 192 | private async onload_internal(): Promise 193 | { 194 | // Set this as THE instance 195 | InlineScriptsPlugin._instance = this; 196 | 197 | // Load settings 198 | this.settings = await this.loadData(); 199 | if (this.settings && !this.settings.version) { this.settings.version = 0; } 200 | this.settings = Object.assign(InlineScriptsPlugin.getDefaultSettings(), this.settings); 201 | 202 | // Auto-convert old-versioned settings (because fixing this manually is hard for the user) 203 | this.settings.shortcuts = this.settings.shortcuts.replaceAll("\n~~\n", "\n__\n"); 204 | 205 | // Now that settings are loaded, update variable for the suffix's final character 206 | this.suffixEndCharacter = this.settings.suffix.charAt(this.settings.suffix.length - 1); 207 | 208 | // Attach settings UI 209 | this.settingsUi = new InlineScriptsPluginSettings(this); 210 | this.addSettingTab(this.settingsUi); 211 | 212 | // Attach buttons view 213 | ButtonView.staticConstructor(); 214 | 215 | // Attach autocomplete feature 216 | this._autocomplete = new AutoComplete(this) 217 | this.registerEditorSuggest(this._autocomplete); 218 | 219 | // Add this plugin to "_inlineScripts.inlineScripts" 220 | HelperFncs.confirmObjectPath("_inlineScripts.inlineScripts.plugin", this); 221 | 222 | // Initialize support objects 223 | ShortcutExpander.staticConstructor(); 224 | ShortcutLinks.staticConstructor(); 225 | HelperFncs.staticConstructor(); 226 | this.shortcutDfc = new Dfc( 227 | this.getActiveShortcutFileAddresses(), ShortcutLoader.getFunction_setupShortcuts(), 228 | this.runShutdownScript.bind(this), true); 229 | this.shortcutDfc.setMonitorType( 230 | this.settings.devMode ? DfcMonitorType.OnTouch : DfcMonitorType.OnModify); 231 | 232 | //Setup bound verson of this function for persistant use 233 | this._cm5_handleExpansionTrigger = this.cm5_handleExpansionTrigger.bind(this); 234 | this._runAllShutdownScripts = this.runAllShutdownScripts.bind(this); 235 | 236 | // Connect "code mirror 5" instances to this plugin to trigger expansions 237 | this.registerCodeMirror( (cm: any) => cm.on("keydown", this._cm5_handleExpansionTrigger) ); 238 | 239 | // Setup "code mirror 6" editor extension management to trigger expansions 240 | this.registerEditorExtension([ 241 | require("@codemirror/state").EditorState.transactionFilter.of( 242 | this.cm6_handleExpansionTrigger.bind(this)) 243 | ]); 244 | 245 | // Call all shutdown scripts of shortcut-files 246 | this.app.workspace.on("quit", this._runAllShutdownScripts); 247 | 248 | // Log that the plugin has loaded 249 | UserNotifier.run( 250 | { 251 | consoleMessage: "Loaded (" + this.manifest.version + ")", 252 | messageLevel: "info" 253 | }); 254 | 255 | await this.showAnnouncements(); 256 | } 257 | 258 | private async onunload_internal(): Promise 259 | { 260 | // Remove the button view 261 | ButtonView.staticDestructor(); 262 | 263 | // Shutdown the shortcutDfc 264 | this.shortcutDfc.destructor(); 265 | 266 | // Shutdown the AutoComplete 267 | this._autocomplete.destructor(); 268 | 269 | // Call all shutdown scripts of shortcut-files 270 | await this.runAllShutdownScripts(); 271 | this.app.workspace.off("quit", this._runAllShutdownScripts); 272 | 273 | // Disconnect "code mirror 5" instances from this plugin 274 | this.app.workspace.iterateCodeMirrors( 275 | (cm: any) => cm.off("keydown", this._cm5_handleExpansionTrigger)); 276 | 277 | // Remove the plugin global state 278 | delete window._inlineScripts; 279 | 280 | // Log that the plugin has unloaded 281 | UserNotifier.run( 282 | { 283 | consoleMessage: "Unloaded (" + this.manifest.version + ")", 284 | messageLevel: "info" 285 | }); 286 | } 287 | 288 | // Call the given shortcut-file's shutdown script. 289 | // Note: This is called when shortcut-file is being disabled 290 | private async runShutdownScript(filename: string): Promise 291 | { 292 | if (!this.shutdownScripts[filename]) { return; } 293 | try 294 | { 295 | await ShortcutExpander.runExpansionScript( 296 | this.shutdownScripts[filename], false, { shortcutText: "sfile shutdown" }); 297 | } 298 | catch (e: any) {} 299 | delete this.shutdownScripts[filename]; 300 | } 301 | 302 | // Shutdown all scripts 303 | private async runAllShutdownScripts(): Promise 304 | { 305 | for (const filename in this.shutdownScripts) 306 | { 307 | await this.runShutdownScript(filename); 308 | } 309 | } 310 | 311 | 312 | // CM5 callback for "keydown". Used to kick off shortcut expansion attempt. 313 | private cm5_handleExpansionTrigger(cm: any, keydown: KeyboardEvent): void 314 | { 315 | // Handle blocking key inputs when input is disabled 316 | if (this.inputDisabled) 317 | { 318 | event.preventDefault(); 319 | } 320 | 321 | if ((event as any)?.key === this.suffixEndCharacter) 322 | { 323 | this.tryShortcutExpansion(); 324 | } 325 | } 326 | 327 | // CM6 callback for editor events. Used to kick off shortcut expansion attempt. 328 | private cm6_handleExpansionTrigger(tr: any): any 329 | { 330 | // Handle blocking key inputs when input is disabled 331 | if (this.inputDisabled) 332 | { 333 | return null; 334 | } 335 | 336 | // Only bother with key inputs that have changed the document 337 | if (!tr.isUserEvent("input.type") || !tr.docChanged) { return tr; } 338 | 339 | let shouldTryExpansion: boolean = false; 340 | 341 | // Iterate over each change made to the document 342 | tr.changes.iterChanges( 343 | (fromA: number, toA: number, fromB: number, toB: number, inserted: any) => 344 | { 345 | // Only try expansion if the shortcut suffix's end character was hit 346 | if (inserted.text[0] === this.suffixEndCharacter) 347 | { 348 | shouldTryExpansion = true; 349 | } 350 | }, false); 351 | 352 | if (shouldTryExpansion) 353 | { 354 | this.tryShortcutExpansion(); 355 | } 356 | 357 | return tr; 358 | } 359 | 360 | // Tries to get shortcut beneath caret and expand it. setTimeout pauses for a frame to 361 | // give the calling event the opportunity to finish processing. This is especially 362 | // important for CM5, as the typed key isn't in the editor until the calling event finishes. 363 | private tryShortcutExpansion_internal(): void { setTimeout(async () => 364 | { 365 | const editor: any = this.app.workspace.getActiveViewOfType(MarkdownView)?.editor; 366 | if (!editor) { return; } 367 | 368 | // Find bounds of the shortcut beneath the caret (if there is one) 369 | const cursor: any = editor.getCursor(); 370 | const lineText: string = editor.getLine(cursor.line); 371 | const prefixIndex: number = lineText.lastIndexOf(this.settings.prefix, cursor.ch); 372 | const suffixIndex: number = lineText.indexOf( 373 | this.settings.suffix, prefixIndex + this.settings.prefix.length); 374 | 375 | // If the caret is not at a shortcut, early-out 376 | if (prefixIndex === -1 || suffixIndex === -1 || 377 | (suffixIndex + this.settings.suffix.length) < cursor.ch) 378 | { 379 | return; 380 | } 381 | 382 | // Get the shortcut text to expand, and the info on this expansion 383 | const shortcutText: string = 384 | lineText.slice(prefixIndex + this.settings.prefix.length, suffixIndex); 385 | const expansionInfo: any = 386 | { 387 | isUserTriggered: true, 388 | line: lineText, 389 | inputStart: prefixIndex, 390 | inputEnd: suffixIndex + this.settings.suffix.length, 391 | shortcutText: shortcutText, 392 | prefix: this.settings.prefix, 393 | suffix: this.settings.suffix 394 | }; 395 | 396 | // Disable input during the exansion (in case it takes a while) 397 | InputBlocker.setEnabled(true); 398 | 399 | // Run the expansion 400 | let expansionText: string = null; 401 | try 402 | { 403 | expansionText = await ShortcutExpander.expand(shortcutText, false, expansionInfo); 404 | } 405 | catch (e) {} 406 | 407 | // Enable input, now that the expansion is over 408 | InputBlocker.setEnabled(false); 409 | 410 | if (expansionText === null) { return; } 411 | 412 | // Handle a string array from the Expansion result 413 | if (Array.isArray(expansionText)) 414 | { 415 | expansionText = expansionText.join(""); 416 | } 417 | 418 | // Make sure we have a proper string 419 | expansionText = expansionText + ""; 420 | 421 | // Replace written shortcut with Expansion result 422 | editor.replaceRange( 423 | expansionText, 424 | { line: cursor.line, ch: prefixIndex }, 425 | { line: cursor.line, ch: suffixIndex + this.settings.suffix.length } ); 426 | }, 0); } 427 | 428 | private async showAnnouncements() 429 | { 430 | if (this.settings.version === this.manifest.version) { return; } 431 | 432 | const toDisplay = []; 433 | for (const announcement of ANNOUNCEMENTS) 434 | { 435 | if (HelperFncs.versionCompare(announcement.version, this.manifest.version) <= 0 && 436 | HelperFncs.versionCompare(announcement.version, this.settings.version) > 0) 437 | { 438 | let title = "Inline Scripts\n"; 439 | if (HelperFncs.versionCompare(announcement.version, "0.21.0") === 0) 440 | { 441 | title += "(formerly Text Expander JS)\n"; 442 | } 443 | toDisplay.push( 444 | title + announcement.version + "\n\n
" + 445 | announcement.message + "
"); 446 | } 447 | } 448 | for (let i = 0; i < toDisplay.length; i++) 449 | { 450 | const messageCounter = 451 | "
Message " + (i+1) + "/" + toDisplay.length + 452 | "
"; 453 | await Popups.getInstance().alert(messageCounter + toDisplay[i]); 454 | } 455 | this.settings.version = this.manifest.version; 456 | this.saveSettings(); 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /src/defaultSettings.ts: -------------------------------------------------------------------------------- 1 | ///////////////////////////// 2 | // Default plugin settings // 3 | ///////////////////////////// 4 | 5 | "use strict"; 6 | 7 | export const DEFAULT_SETTINGS: any = Object.freeze( 8 | { 9 | prefix: ";;", 10 | suffix: "::", 11 | expansionPrefix: "", 12 | expansionLinePrefix: "> ", 13 | expansionSuffix: "\\n\\n", 14 | autocomplete: true, 15 | autocompleteHelp: true, 16 | devMode: false, 17 | allowExternal: false, 18 | version: (app as any).plugins.manifests["obsidian-text-expander-js"].version, 19 | shortcutFiles: [], 20 | shortcuts: ` 21 | __ 22 | ^hi$ 23 | __ 24 | return "Hello! How are you?"; 25 | __ 26 | hi - Expands into "Hello! How are you?". A simple shortcut to see if Inline Scripts plugin is running. 27 | 28 | __ 29 | ^date$ 30 | __ 31 | return new Date().toLocaleDateString(); 32 | __ 33 | date - Expands into the current, local date. 34 | 35 | __ 36 | ^time$ 37 | __ 38 | return new Date().toLocaleTimeString(); 39 | __ 40 | time - Expands into the current, local time. 41 | 42 | __ 43 | ^datetime$ 44 | __ 45 | return new Date().toLocaleString(); 46 | __ 47 | datetime - Expands into the current, local date and time. 48 | 49 | __ 50 | __ 51 | function roll(max) { return Math.trunc(Math.random() * max + 1); } 52 | __ 53 | A dice roller function used in other shortcuts. 54 | 55 | __ 56 | ^[d|D]([0-9]+)$ 57 | __ 58 | return "🎲 __" + roll($1) + "__ /D" + $1; 59 | __ 60 | d{max: >0} - A dice roller shortcut. Expands into "🎲 {roll result} /D{max}". {max} is the size of dice to roll. 61 | - Examples - d3, d20, d57, d999 62 | 63 | __ 64 | ^([0-9]*)[d|D]([1-9][0-9]*)(|(?:[\+\-][0-9]+))$ 65 | __ 66 | $1 = Number($1) || 1; 67 | $3 ||= "+0"; 68 | let result = 0; 69 | let label = "D" + $2; 70 | if ($1 > 1) { label += "x" + $1; } 71 | for (let i = 0; i < $1; i++) { result += roll($2); } 72 | if (Number($3)) { 73 | result += Number($3); 74 | label += $3; 75 | } 76 | if (isNaN(label.slice(1))) { label = "(" + label + ")"; } 77 | return "🎲 __" + result + "__ /" + label; 78 | __ 79 | {count: >0, default: 1}d{max: >0}{add: + or \\- followed by >0, default: +0} - A dice roller shortcut, same as d{max}, but with optional {count} and {add} parameters. {count} is the number of dice to roll and add together. {add} is "+" or "-" followed by an amount to adjust the result by. 80 | - Examples - d100, 3d20, d10+5, 3d6+6 81 | `, 82 | buttonView: { visible: true, groups: {} } 83 | }); 84 | -------------------------------------------------------------------------------- /src/esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | esbuild.build({ 3 | entryPoints: [ "_Plugin.ts" ], 4 | outfile: "../main.js", 5 | bundle: true, 6 | sourcemap: false, 7 | minify: (process.argv.includes("--minify")), 8 | external: [ "obsidian", "@codemirror/state", "acorn" ], 9 | format: 'cjs', 10 | target: 'es2021', 11 | }).catch(() => process.exit(1)); -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-text-expander-js", 3 | "version": "0.16.14", 4 | "description": "This Obsidian plugin allows the user to type text shortcuts which are then replaced with JavaScript generated text.", 5 | "main": "main.js", 6 | "scripts": { 7 | "c": "clear & node esbuild.config.mjs", 8 | "ts": "clear & tsc", 9 | "release": "clear & node esbuild.config.mjs --minify" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/jon-heard/obsidian-inline-scripts.git" 14 | }, 15 | "keywords": [], 16 | "author": "Jonathan Heard", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/jon-heard/obsidian-inline-scripts/issues" 20 | }, 21 | "homepage": "https://github.com/jon-heard/obsidian-inline-scripts", 22 | "devDependencies": { 23 | "@types/node": "^18.0.1", 24 | "acorn": "^8.8.0", 25 | "esbuild": "^0.14.51", 26 | "obsidian": "^0.15.4", 27 | "tslib": "^2.4.0", 28 | "typescript": "^4.7.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": 3 | { 4 | // Basic settings 5 | "baseUrl": ".", 6 | "outFile": "../main.js", 7 | "target": "ES2021", 8 | "lib": [ "DOM", "ES2021" ], 9 | "module": "system", 10 | "moduleResolution": "node", 11 | 12 | // Debug info 13 | "inlineSourceMap": false, 14 | "inlineSources": false, 15 | 16 | // Error @ compile as much as possible 17 | "noImplicitAny": true, 18 | "checkJs": true, 19 | "noImplicitReturns": true, 20 | "noUnusedLocals":true, 21 | "strict": true, 22 | "strictNullChecks": false, 23 | 24 | // Minify output 25 | "removeComments": true 26 | }, 27 | 28 | // File ordering 29 | "include": 30 | [ 31 | "globals.ts", 32 | "*.ts" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /src/ui_InputBlocker.ts: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // Input blocker - Turn on/off input blocking in the UI. Darkens the screen while blocking input // 3 | //////////////////////////////////////////////////////////////////////////////////////////////////// 4 | 5 | "use strict"; 6 | 7 | import InlineScriptsPlugin from "./_Plugin"; 8 | 9 | export namespace InputBlocker 10 | { 11 | // Adds/removes a tinted full-screen div to prevent user-input 12 | export function setEnabled(value: boolean): void 13 | { 14 | if (value) 15 | { 16 | // If Input blocker UI already exists, do nothing 17 | if (document.getElementById("iscript_inputBlocker")) { return; } 18 | 19 | // Create the input blocker UI 20 | let blockerUi: any = document.createElement("div"); 21 | blockerUi.id = "iscript_inputBlocker"; 22 | blockerUi.classList.add("iscript_preFadein"); 23 | document.getElementsByTagName("body")[0].prepend(blockerUi); 24 | 25 | // Animate fadin 26 | window.getComputedStyle(blockerUi).opacity; 27 | blockerUi.classList.remove("iscript_preFadein"); 28 | 29 | // Enable editor input blocking 30 | InlineScriptsPlugin.getInstance().inputDisabled = true; 31 | } 32 | else 33 | { 34 | // Remove the input blocker UI 35 | let blockerUi: any = document.getElementById("iscript_inputBlocker"); 36 | if (blockerUi) { blockerUi.remove(); } 37 | 38 | // Disable editor input blocking 39 | InlineScriptsPlugin.getInstance().inputDisabled = false; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/ui_Popups.ts: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // Popup - A single class that handles all the kinds of popup modals we need // 3 | /////////////////////////////////////////////////////////////////////////////// 4 | 5 | "use strict"; 6 | 7 | import InlineScriptsPlugin from "./_Plugin"; 8 | import { Modal, Setting } from "obsidian"; 9 | 10 | // Custom popup definition: 11 | // buttonsIds - Which buttons show at the bottom of the popup dialog 12 | // onOpen(data, parent, firstButton, SettingType) - Allows creating the ui for this popup. 13 | // data - An object for relaying info: passed in and passed to both onOpen() and onClose(). 14 | // parent - The html div to contain the popup ui. 15 | // firstButton - The first html button. Useful for triggering popup completion from code. 16 | // SettingType - An Obsidian type to instantiate. Provides an API for creating ui. 17 | // resolveFnc - The function to call with the result of the popup (return true to early- 18 | // out, call this first to set return value). 19 | // onClose(data, resolveFnc, buttonId) - Called when the user is finished deciding. 20 | // data - An object for relaying info: passed in and passed to both onOpen() and onClose(). 21 | // resolveFnc - The function to call with the result of the popup. 22 | // buttonId - The id of button that was clicked (or null if no button was clicked). 23 | 24 | const CLOSE_CHECK_INTERVAL = 250; 25 | 26 | export class Popups extends Modal 27 | { 28 | // Singleton getter 29 | public static getInstance(): Popups 30 | { 31 | return this.getInstance_internal(); 32 | } 33 | 34 | // A popup conveying a message until the user presses the "Ok" button 35 | public async alert(message: string, buttonLabel?: string): Promise 36 | { 37 | await this.alert_internal(message, buttonLabel); 38 | } 39 | 40 | // A popup asking for a confirmation until the user clicks the "Confirm" or "Cancel" button 41 | public async confirm(message: string, buttonLabels?: Array): Promise 42 | { 43 | return await this.confirm_internal(message, buttonLabels); 44 | } 45 | 46 | // A popup asking for some text until the user clicks the "Ok" or "Cancel" button 47 | public async input( 48 | message: string, defaultValue?: string, suggestions?: Array, 49 | buttonLabels?: Array) : Promise 50 | { 51 | return await this.input_internal(message, defaultValue, suggestions, buttonLabels); 52 | } 53 | 54 | // A popup asking for selection from a list until the user clicks the "Ok" or "Cancel" button 55 | public async pick( 56 | message: string, options: Array, defaultValue?: number, showCount?: number, 57 | buttonLabels?: Array): Promise 58 | { 59 | return await this.pick_internal(message, options, defaultValue, showCount, buttonLabels); 60 | } 61 | 62 | // A popup that works based on a custom popup definition. Closes when the user clicks a button. 63 | public async custom(message: string, definition: any, data?: any, buttonLabels?: Array) 64 | : Promise 65 | { 66 | return await this.custom_internal(message, definition, data, buttonLabels); 67 | } 68 | 69 | /////////////////////////////////////////////////////////////////////////////////////////////////// 70 | 71 | // Override inherited method 72 | public onOpen(): void 73 | { 74 | this.onOpen_internal(); 75 | } 76 | 77 | // Override inherited method 78 | public onClose(): void 79 | { 80 | this.onClose_internal(); 81 | } 82 | 83 | /////////////////////////////////////////////////////////////////////////////////////////////////// 84 | 85 | // The function to close the popup and define the return 86 | private _resolve: Function; 87 | // Stores the text of the popup button that was clicked 88 | private _clickedButtonId: string; 89 | 90 | // The user-supplied message 91 | private _message: string; 92 | // Definition for the current popup 93 | private _definition: any; 94 | // The object of parameters for the current popup 95 | private _data: any; 96 | // Optional button labels for customizing the button displays. 97 | private _buttonLabels: Array; 98 | 99 | /////////////////////////////////////////////////////////////////////////////////////////////////// 100 | 101 | // Customiation for an "Alert" poupup 102 | private ALERT_DEFINITION = Object.freeze( 103 | { 104 | buttonIds: [ "Ok" ] 105 | }); 106 | 107 | // Customiation for a "Confirm" poupup 108 | private CONFIRM_DEFINITION = Object.freeze( 109 | { 110 | buttonIds: [ "Confirm", "Cancel" ], 111 | onClose: async (data: any, resolveFnc: Function, buttonId: string) => 112 | { 113 | resolveFnc(buttonId === "Confirm"); 114 | } 115 | }); 116 | 117 | // Customiation for an "Input" poupup 118 | private INPUT_DEFINITION = Object.freeze( 119 | { 120 | onOpen: async (data: any, parent: any, firstButton: any, SettingType: any) => 121 | { 122 | let textUi: any = null; 123 | new SettingType(parent) 124 | .addText((text: any) => 125 | { 126 | data.resultUi = text; 127 | text.setValue("" + (data.defaultValue ?? "")); 128 | text.inputEl.select(); 129 | text.inputEl.parentElement.previousSibling.remove(); 130 | text.inputEl.addEventListener("keypress", (e: any) => 131 | { 132 | if (e.key === "Enter") { firstButton.click(); } 133 | }); 134 | textUi = text.inputEl; 135 | return text; 136 | }) 137 | if (data.suggestions?.length) 138 | { 139 | let suggestionsUi: any = document.createElement("datalist"); 140 | suggestionsUi.id = "suggestionsUi"; 141 | for (const suggestion of data.suggestions) 142 | { 143 | suggestionsUi.appendChild(new Option(suggestion)); 144 | } 145 | textUi.parentNode.appendChild(suggestionsUi); 146 | textUi.setAttr("list", "suggestionsUi"); 147 | } 148 | }, 149 | onClose: async (data: any, resolveFnc: Function, buttonId: string) => 150 | { 151 | resolveFnc((buttonId === "Ok") ? data.resultUi.getValue() : null); 152 | } 153 | }); 154 | 155 | // Customiation for a "Pick" poupup 156 | private PICK_DEFINITION = Object.freeze( 157 | { 158 | onOpen: async (data: any, parent: any, firstButton: any, SettingType: any) => 159 | { 160 | let result = false; 161 | new SettingType(parent) 162 | .addDropdown((dropdown: any) => 163 | { 164 | data.resultUi = dropdown; 165 | 166 | let options = data.options; 167 | if (options === null || options === undefined) 168 | { 169 | result = true; // "result" is outside this function. Set it, then quit. 170 | return; 171 | } 172 | if (!Array.isArray(options)) { options = [ options ]; } 173 | 174 | let defaultValue = parseInt(data.defaultValue ?? 0); 175 | if (isNaN(defaultValue)) { defaultValue = options.indexOf(data.defaultValue); } 176 | defaultValue = Math.clamp(defaultValue, 0, options.length - 1); 177 | 178 | dropdown.addOptions(options); 179 | dropdown.setValue(defaultValue || 0); 180 | if (isNaN(data.showCount)) 181 | { 182 | if (data.showCount.toLowerCase().startsWith("adaptive")) 183 | { 184 | const max = 185 | data.showCount.toLowerCase().match(/adaptive:([0-9]+)/)?.[1] || 20; 186 | data.showCount = Math.min(data.options.length, max); 187 | } 188 | else 189 | { 190 | data.showCount = 1; 191 | } 192 | } 193 | if (data.showCount > 1) 194 | { 195 | dropdown.selectEl.setAttr("size", data.showCount); 196 | dropdown.selectEl.classList.add("iscript_listSelect"); 197 | } 198 | dropdown.selectEl.parentElement.previousSibling.remove(); 199 | dropdown.selectEl.addEventListener("keypress", (e: any) => 200 | { 201 | if (e.key === "Enter") { firstButton.click(); } 202 | }); 203 | return dropdown; 204 | }); 205 | return result; 206 | }, 207 | onClose: async (data: any, resolveFnc: Function, buttonId: string) => 208 | { 209 | resolveFnc((buttonId === "Ok") ? Number(data.resultUi.getValue()) : null); 210 | } 211 | }); 212 | 213 | /////////////////////////////////////////////////////////////////////////////////////////////////// 214 | 215 | // Singleton instance 216 | private static _instance: Popups; 217 | 218 | // Singleton getter 219 | private static getInstance_internal(): Popups 220 | { 221 | if (!this._instance) { this._instance = new Popups(); } 222 | return this._instance; 223 | } 224 | 225 | // Private constructor for singleton 226 | private constructor() 227 | { 228 | super(InlineScriptsPlugin.getInstance().app); 229 | this.modalEl.classList.add("iscript_popup"); 230 | } 231 | 232 | // An event handler used for all buttons - records the button id, then closes the popup 233 | private onButton(this: any): void 234 | { 235 | const p = Popups.getInstance(); 236 | p._clickedButtonId = this.dataset.id; 237 | p.close(); 238 | } 239 | 240 | /////////////////////////////////////////////////////////////////////////////////////////////////// 241 | 242 | // A popup conveying a message until the user presses the "Ok" button 243 | private async alert_internal(message: string, buttonLabel?: string): Promise 244 | { 245 | return await this.custom(message, this.ALERT_DEFINITION, null, [ buttonLabel ]); 246 | } 247 | 248 | // A popup asking for a confirmation until the user clicks the "Confirm" or "Cancel" button 249 | private async confirm_internal(message: string, buttonLabels?: Array): Promise 250 | { 251 | return await this.custom(message, this.CONFIRM_DEFINITION, null, buttonLabels); 252 | } 253 | 254 | // A popup asking for some text until the user clicks the "Ok" or "Cancel" button 255 | public async input_internal( 256 | message: string, defaultValue?: string, suggestions?: Array, 257 | buttonLabels?: Array) : Promise 258 | { 259 | return await this.custom( 260 | message, this.INPUT_DEFINITION, { defaultValue, suggestions }, buttonLabels); 261 | } 262 | 263 | // A popup asking for selection from a list until the user clicks the "Ok" or "Cancel" button 264 | public async pick_internal( 265 | message: string, options: Array, defaultValue?: number, showCount?: number, 266 | buttonLabels?: Array): Promise 267 | { 268 | showCount ||= 1; 269 | return await this.custom( 270 | message, this.PICK_DEFINITION, { options, defaultValue, showCount }, buttonLabels); 271 | } 272 | 273 | // A popup that works based on a custom popup definition. Closes when the user clicks a button. 274 | public async custom_internal( 275 | message: string, definition: any, data?: any, buttonLabels?: Array): Promise 276 | { 277 | // Initialize the popup with sanitized parameters 278 | this._message = message ?? ""; 279 | this._definition = definition || {}; 280 | this._data = data || {}; 281 | this._buttonLabels = buttonLabels || []; 282 | this._clickedButtonId = null; 283 | // Return a promise, which resolves once the user clicks one of the buttons 284 | return await new Promise((resolve) => 285 | { 286 | // Initialize the popup 287 | this._resolve = resolve; 288 | // Open the popup 289 | this.open(); 290 | }); 291 | } 292 | 293 | /////////////////////////////////////////////////////////////////////////////////////////////////// 294 | 295 | private async onOpen_internal(): Promise 296 | { 297 | // Hide input blocker dimmer 298 | let inputBlockerDimmer = document.getElementById("iscript_inputBlocker"); 299 | if (inputBlockerDimmer) { inputBlockerDimmer.style.display = "none"; } 300 | 301 | // Clear UI 302 | this.contentEl.setText(""); 303 | this.titleEl.setText(""); 304 | 305 | // Setup message 306 | if (typeof this._message === "string") 307 | { 308 | this.titleEl.innerHTML = this._message.replaceAll("\n", "
"); 309 | } 310 | else if (this._message) 311 | { 312 | this.titleEl.innerHTML = this._message; 313 | } 314 | 315 | // Setup type-specific ui container 316 | const typeSpecificUi: any = document.createElement("div"); 317 | this.contentEl.appendChild(typeSpecificUi); 318 | 319 | // Setup buttons 320 | let firstButton: any = null; 321 | const buttonsUi = new Setting(this.contentEl); 322 | buttonsUi.settingEl.style.padding = "0"; 323 | const buttonIds = this._definition.buttonIds || [ "Ok", "Cancel" ]; 324 | for (let i = 0; i < buttonIds.length; i++) 325 | { 326 | buttonsUi.addButton((button: any) => 327 | { 328 | button 329 | .setButtonText(this._buttonLabels[i] || buttonIds[i]) 330 | .onClick(this.onButton.bind(button.buttonEl)); 331 | button.buttonEl.dataset.id = buttonIds[i]; 332 | if (!firstButton) 333 | { 334 | button.setCta(); 335 | firstButton = button.buttonEl; 336 | } 337 | }); 338 | } 339 | 340 | // Setup type-specific ui 341 | if (this._definition.onOpen) 342 | { 343 | // If type-specific onOpen returns true, we should early out 344 | if (await this._definition.onOpen( 345 | this._data, typeSpecificUi, firstButton, Setting, this._resolve)) 346 | { 347 | this._definition = {}; 348 | this.close(); 349 | } 350 | } 351 | 352 | // Closing logic. Needs to happen here instead of onClose as onClose doesn't work with 353 | // sequential popups when on mobile. 354 | let looper = setInterval(async () => 355 | { 356 | if (!this.contentEl.parentNode.parentNode.parentNode) 357 | { 358 | clearInterval(looper); 359 | 360 | // Call type-specific onClose 361 | if (this._definition.onClose) 362 | { 363 | await this._definition.onClose( 364 | this._data, this._resolve, this._clickedButtonId); 365 | } 366 | 367 | // Do extra resolve, in case _onClose isn't available, or didn't end up resolving. 368 | this._resolve(null); 369 | 370 | } 371 | }, CLOSE_CHECK_INTERVAL); 372 | } 373 | 374 | private onClose_internal(): void 375 | { 376 | // Unhide blocker dimmer 377 | const inputBlockerDimmer = document.getElementById("iscript_inputBlocker"); 378 | if (inputBlockerDimmer) { inputBlockerDimmer.style.display = "unset"; } 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /src/ui_dragReorder.ts: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////// 2 | // DragReorder - A class to handle reordering html elements through drag-and-drop. // 3 | ////////////////////////////////////////////////////////////////////////////////// 4 | 5 | "use strict"; 6 | 7 | export class DragReorder 8 | { 9 | preDragDistanceSqr: number = 200; 10 | container: any = null; 11 | items: Array = null; 12 | dragged: any = null; 13 | downPoint: Array = null; 14 | onDragged: Function = null; 15 | 16 | constructor(container: any, onDragged?: Function, dragScale?: number) 17 | { 18 | dragScale ||= 0.5; 19 | this.onDragged = onDragged; 20 | this.container = container; 21 | container.classList.add("iscript_drag_container"); 22 | const emSize = 23 | Number(window.getComputedStyle(container, null). 24 | getPropertyValue("font-size").replace("px","")); 25 | this.preDragDistanceSqr = Math.pow(emSize, 2) * dragScale; 26 | this.items = []; 27 | for (let item of container.childNodes) 28 | { 29 | if (!item.classList) { continue; } 30 | this.items.push(item); 31 | item.classList.add("iscript_drag_item"); 32 | item.addEventListener("pointerdown", (evt: any) => 33 | { 34 | this.downPoint = [ evt.offsetX, evt.offsetY ]; 35 | }); 36 | item.addEventListener("pointermove", (evt: any) => 37 | { 38 | if (this.dragged || !this.downPoint) { return; } 39 | const distance = 40 | Math.pow(this.downPoint[0] - evt.offsetX, 2) + 41 | Math.pow(this.downPoint[1] - evt.offsetY, 2); 42 | if (distance > this.preDragDistanceSqr) 43 | { 44 | this.initDrag(evt.target); 45 | } 46 | }); 47 | item.addEventListener("pointerenter", (evt: any) => 48 | { 49 | if (this.dragged && evt.target !== this.dragged) 50 | { 51 | this.dragOver(evt.target); 52 | } 53 | }); 54 | item.addEventListener("pointerout", (evt: any) => 55 | { 56 | if (this.dragged || !this.downPoint) { return; } 57 | this.initDrag(evt.target); 58 | }); 59 | item.addEventListener("gotpointercapture", 60 | (e: any) => e.target.releasePointerCapture(e.pointerId)); 61 | } 62 | document.addEventListener("pointerup", () => 63 | { 64 | if (this.downPoint) { this.downPoint = null; } 65 | if (this.dragged) { this.endDrag(); } 66 | }); 67 | } 68 | 69 | initDrag(item: any): void 70 | { 71 | this.dragged = item; 72 | this.container.classList.add("iscript_drag_container_dragging"); 73 | this.dragged.classList.add("iscript_drag_item_dragged"); 74 | for (const item of this.items) 75 | { 76 | if (item === this.dragged) { continue; } 77 | item.classList.add("iscript_drag_item_notDragged"); 78 | } 79 | } 80 | 81 | dragOver(target: any): void 82 | { 83 | for (const child of this.container.getElementsByClassName("iscript_drag_item")) 84 | { 85 | if (child === this.dragged) 86 | { 87 | this.dragged.parentNode.insertBefore(this.dragged, target); 88 | this.dragged.parentNode.insertBefore(target, this.dragged); 89 | break; 90 | } 91 | else if (child === target) 92 | { 93 | this.dragged.parentNode.insertBefore(this.dragged, target); 94 | break; 95 | } 96 | } 97 | } 98 | 99 | endDrag(): void 100 | { 101 | this.container.classList.remove("iscript_drag_container_dragging"); 102 | this.dragged.classList.remove("iscript_drag_item_dragged"); 103 | for (const item of this.items) 104 | { 105 | item.classList.remove("iscript_drag_item_notDragged"); 106 | } 107 | this.dragged = null; 108 | this.onDragged?.(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/ui_setting_actions.ts: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////////// 2 | // Setting ui actions - Buttons to perform Inline Scripts actions. // 3 | ///////////////////////////////////////////////////////////////////// 4 | 5 | "use strict"; 6 | 7 | import InlineScriptsPlugin from "./_Plugin"; 8 | import { ButtonView } from "./ui_ButtonView"; 9 | import { Popups } from "./ui_Popups"; 10 | 11 | export abstract class SettingUi_Actions 12 | { 13 | // Create the setting ui 14 | public static create(parent: any): void 15 | { 16 | return this.create_internal(parent); 17 | } 18 | 19 | /////////////////////////////////////////////////////////////////////////////////////////////////// 20 | 21 | private static create_internal(parent: any): void 22 | { 23 | // Actions header & container 24 | parent.createEl("div").innerHTML = " "; 25 | parent.createEl("h2", { text: "Actions" }); 26 | const actionsDiv = parent.createEl("div", { cls: "iscript_actionsSection" }); 27 | 28 | // Open button-view button 29 | const openButtonsView = 30 | actionsDiv.createEl("button", { text: "Open buttons view" }); 31 | openButtonsView.toggleClass("iscript_button_disabled", ButtonView.isOpen()); 32 | openButtonsView.onclick = async () => 33 | { 34 | // Ignore disabled button 35 | if (openButtonsView.classList.contains("iscript_button_disbled")) { return; } 36 | 37 | // Add buttonview 38 | ButtonView.activateView(true); 39 | 40 | // Disable this button 41 | openButtonsView.toggleClass("iscript_button_disabled", true); 42 | }; 43 | 44 | //Space 45 | actionsDiv.createEl("div").innerHTML = " "; 46 | 47 | // Reset settings button 48 | const resetSettings = actionsDiv.createEl("button", { text: "Reset settings to defaults" }); 49 | resetSettings.onclick = async () => 50 | { 51 | const plugin = InlineScriptsPlugin.getInstance(); 52 | if (await Popups.getInstance().confirm( 53 | "Confirm resetting ALL settings to their default values.")) 54 | { 55 | plugin.settings = InlineScriptsPlugin.getDefaultSettings(); 56 | plugin.settingsUi.display(); 57 | plugin.settings.shortcuts = ""; 58 | } 59 | }; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/ui_setting_alerts.ts: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // Setting ui alerts - Alert ribbons that show when the plugin or library has updates available. // 3 | /////////////////////////////////////////////////////////////////////////////////////////////////// 4 | 5 | "use strict"; 6 | 7 | import InlineScriptsPlugin from "./_Plugin"; 8 | import { HelperFncs } from "./HelperFncs"; 9 | 10 | export abstract class SettingUi_Alerts 11 | { 12 | // Create the setting ui 13 | public static create(parent: any): void 14 | { 15 | return this.create_internal(parent); 16 | } 17 | 18 | /////////////////////////////////////////////////////////////////////////////////////////////////// 19 | 20 | private static create_internal(parent: any): void 21 | { 22 | // Plugin update alert 23 | let alert_pluginUpdate = parent.createEl("div", { cls: "iscript_alert iscript_hidden" }); 24 | alert_pluginUpdate.innerText = "Plugin update available"; 25 | alert_pluginUpdate.style["margin-top"] = "1em"; 26 | 27 | // Plugin update check 28 | (async () => { try { 29 | // Get the latest version 30 | const latestVersion = (await window.request( 31 | { 32 | url: "https://api.github.com/repos/jon-heard/obsidian-inline-scripts/releases/latest", 33 | method: "GET", headers: { "Cache-Control": "no-cache" } 34 | })).match(/\"name\": \"(.*)\"/)[1]; 35 | 36 | // Get the current version 37 | const currentVersion = InlineScriptsPlugin.getInstance().manifest.version; 38 | 39 | if (HelperFncs.versionCompare(currentVersion, latestVersion) < 0) 40 | { 41 | alert_pluginUpdate.toggleClass("iscript_hidden", false); 42 | alert_pluginUpdate.innerHTML += ":   " + latestVersion + ""; 43 | } 44 | } catch {} })(); 45 | 46 | // Library update alert 47 | let alert_libraryUpdate = parent.createEl("div", { cls: "iscript_alert iscript_hidden" }); 48 | alert_libraryUpdate.innerHTML = "Library update available (re-import)"; 49 | alert_libraryUpdate.style["margin-top"] = "1em"; 50 | alert_libraryUpdate.id = "alert_libraryUpdates"; 51 | 52 | // Library update check 53 | (async () => { try { 54 | const plugin = InlineScriptsPlugin.getInstance(); 55 | 56 | // Get the latest version 57 | const latestVersion = (await window.request( 58 | { 59 | url: "https://raw.githubusercontent.com/jon-heard/obsidian-inline-scripts-library/main/README.md", 60 | method: "GET", headers: { "Cache-Control": "no-cache" } 61 | })).match(/# Version (.*)/)[1] || ""; 62 | 63 | // Get the current version 64 | let versionFilePath = ""; 65 | const shortcutFiles = plugin.settings.shortcutFiles; 66 | for (const shortcutFile of shortcutFiles) 67 | { 68 | if (shortcutFile.address.endsWith("state.sfile.md")) 69 | { 70 | versionFilePath = shortcutFile.address.slice(0, -14) + "Ξ_libraryVersion.md"; 71 | break; 72 | } 73 | } 74 | const versionFile = (plugin.app.vault as any).fileMap[versionFilePath]; 75 | const currentVersion = 76 | !versionFile ? "" : (await plugin.app.vault.cachedRead(versionFile)) || ""; 77 | 78 | if (HelperFncs.versionCompare(currentVersion, latestVersion) < 0) 79 | { 80 | alert_libraryUpdate.toggleClass("iscript_hidden", false); 81 | alert_libraryUpdate.innerHTML += ":   " + latestVersion + ""; 82 | } 83 | } catch {} })(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/ui_setting_expansionFormat.ts: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////////////////////// 2 | // Setting ui expansionFormat - Setup an optional prefix and postfix for shortcut expansions. // 3 | //////////////////////////////////////////////////////////////////////////////////////////////// 4 | 5 | "use strict"; 6 | 7 | import { Setting } from "obsidian"; 8 | 9 | export abstract class SettingUi_ExpansionFormat 10 | { 11 | // Create the setting ui 12 | public static create(parent: any, settings: any): void 13 | { 14 | return this.create_internal(parent, settings); 15 | } 16 | 17 | // Get the contents of the setting ui 18 | public static getContents(): any 19 | { 20 | return this._settings; 21 | } 22 | 23 | /////////////////////////////////////////////////////////////////////////////////////////////////// 24 | 25 | private static _settings: any; 26 | 27 | private static create_internal(parent: any, settings: any): void 28 | { 29 | // Settings section 30 | this._settings = 31 | { 32 | expansionPrefix: settings.expansionPrefix, 33 | expansionLinePrefix: settings.expansionLinePrefix, 34 | expansionSuffix: settings.expansionSuffix 35 | }; 36 | 37 | // Title 38 | parent.createEl("h2", { text: "Common expansion format" }); 39 | 40 | // Prefix 41 | new Setting(parent) 42 | .setName("Prefix") 43 | .setDesc("Text added to the start of a formatted expansion.") 44 | .addText((text: any) => 45 | { 46 | return text 47 | .setPlaceholder("") 48 | .setValue(settings.expansionPrefix) 49 | .onChange((value: string) => 50 | { 51 | this._settings.expansionPrefix = value; 52 | }); 53 | }) 54 | .settingEl.toggleClass("iscript_settingBundledTop", true); 55 | 56 | // Line-prefix 57 | new Setting(parent) 58 | .setName("Line-prefix") 59 | .setDesc("Text added to the start of each line of a formatted expansion.") 60 | .addText((text: any) => 61 | { 62 | return text 63 | .setPlaceholder("") 64 | .setValue(settings.expansionLinePrefix) 65 | .onChange((value: string) => 66 | { 67 | this._settings.expansionLinePrefix = value; 68 | }); 69 | }) 70 | .settingEl.toggleClass("iscript_settingBundled", true); 71 | 72 | // Suffix 73 | new Setting(parent) 74 | .setName("Suffix") 75 | .setDesc("Text added to the end of a formatted expansion.") 76 | .addText((text: any) => 77 | { 78 | return text 79 | .setPlaceholder("") 80 | .setValue(settings.expansionSuffix) 81 | .onChange((value: string) => 82 | { 83 | this._settings.expansionSuffix = value; 84 | }); 85 | }) 86 | .settingEl.toggleClass("iscript_settingBundled", true); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/ui_setting_helper.ts: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////// 2 | // Setting ui helper - a collection of functions used by multiple SettingUi classes // 3 | ////////////////////////////////////////////////////////////////////////////////////// 4 | 5 | "use strict"; 6 | 7 | import { Popups } from "./ui_Popups"; 8 | 9 | export namespace SettingUi_Helper 10 | { 11 | export async function deleteButtonClicked(this: any): Promise 12 | { 13 | if (await Popups.getInstance().confirm("Confirm removing a " + this.typeTitle + ".")) 14 | { 15 | this.group.remove(); 16 | } 17 | } 18 | 19 | export function upButtonClicked(this: any): void 20 | { 21 | let p: any = this.group.parentElement; 22 | let index: number = Array.from(p.childNodes).indexOf(this.group); 23 | if (index === this.listOffset) { return; } 24 | p.insertBefore(p.childNodes[index], p.childNodes[index - 1]); 25 | } 26 | 27 | export function downButtonClicked(this: any): void 28 | { 29 | let p: any = this.group.parentElement; 30 | let index: number = Array.from(p.childNodes).indexOf(this.group); 31 | if (index === p.childNodes.length - 1) { return; } 32 | index++; 33 | p.insertBefore(p.childNodes[index], p.childNodes[index - 1]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ui_setting_other.ts: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////// 2 | // Setting ui other - Create and work with the miscelaneous settings not handled elsewhere. // 3 | ////////////////////////////////////////////////////////////////////////////////////////////// 4 | 5 | "use strict"; 6 | 7 | import { Setting, Platform } from "obsidian"; 8 | 9 | export abstract class SettingUi_Other 10 | { 11 | // Create the setting ui 12 | public static create(parent: any, settings: any): void 13 | { 14 | return this.create_internal(parent, settings); 15 | } 16 | 17 | // Get the contents of the setting ui 18 | public static getContents(): any 19 | { 20 | return this._settings; 21 | } 22 | 23 | /////////////////////////////////////////////////////////////////////////////////////////////////// 24 | 25 | private static _settings: any; 26 | 27 | private static create_internal(parent: any, settings: any): void 28 | { 29 | this._settings = 30 | { 31 | autocomplete: settings.autocomplete, 32 | autocompleteHelp: settings.autocompleteHelp, 33 | devMode: settings.devMode, 34 | allowExternal: settings.allowExternal 35 | }; 36 | 37 | parent.createEl("h2", { text: "Other Settings" }); 38 | 39 | // Autocomplete 40 | new Setting(parent) 41 | .setName("Autocomplete") 42 | .setDesc("Enable / disable autocomplete for shortcut entry.") 43 | .addToggle((toggle: any) => 44 | { 45 | return toggle 46 | .setValue(settings.autocomplete === undefined ? true : settings.autocomplete) 47 | .onChange((value: string) => this._settings.autocomplete = value ); 48 | }); 49 | 50 | // Autocomplete help popup 51 | new Setting(parent) 52 | .setName("Autocomplete help popup") 53 | .setDesc("Enable / disable the shortcut descriptor popup during autocomplete.") 54 | .addToggle((toggle: any) => 55 | { 56 | return toggle 57 | .setValue(settings.autocompleteHelp === undefined ? 58 | true : settings.autocompleteHelp) 59 | .onChange((value: string) => this._settings.autocompleteHelp = value ); 60 | }); 61 | 62 | // Developer mode 63 | new Setting(parent) 64 | .setName("Developer mode") 65 | .setDesc("Shortcut-files are monitored for updates if this is on.") 66 | .addToggle((toggle: any) => 67 | { 68 | return toggle 69 | .setValue(settings.devMode) 70 | .onChange((value: string) => this._settings.devMode = value ); 71 | }); 72 | 73 | // Allow external (not available on mobile) 74 | if (!Platform.isMobile) 75 | { 76 | new Setting(parent) 77 | .setName("Allow external") 78 | .setDesc("Shortcuts can run external commands if this is on.") 79 | .addToggle((toggle: any) => 80 | { 81 | return toggle 82 | .setValue(settings.allowExternal) 83 | .onChange((value: string) => this._settings.allowExternal = value ); 84 | }) 85 | .descEl.createEl("div", { 86 | cls: "iscript_alert", 87 | text: "WARNING: enabling this increases the danger from malicious shortcuts" }); 88 | } 89 | 90 | parent.createEl("hr"); 91 | let donationCaption = parent.createEl("div", { cls: "iscript_donationsCaption" }); 92 | donationCaption.innerHTML = 93 | "If you find this plugin useful, then a donation lets me know" + "
" + 94 | "that I should keep working to make it better."; 95 | let donations = parent.createEl("div", { cls: "iscript_donations" }); 96 | donations.innerHTML = 97 | ` 98 | 99 | 100 | 101 | 102 | 103 | ` 104 | + 105 | `Buy Me A Coffee` 106 | ; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/ui_setting_shortcutFiles.ts: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////////////////////////////////// 2 | // Setting ui shortcut files - Create and work with a setting of a list of shortcut-files. // 3 | ///////////////////////////////////////////////////////////////////////////////////////////// 4 | 5 | "use strict"; 6 | 7 | import { Setting, normalizePath } from "obsidian"; 8 | import { SettingUi_Helper } from "./ui_setting_helper"; 9 | import { LibraryImporter } from "./LibraryImporter"; 10 | 11 | export abstract class SettingUi_ShortcutFiles 12 | { 13 | // Create the setting ui 14 | public static create(parent: any, settings: any, app: any): void 15 | { 16 | return this.create_internal(parent, settings, app); 17 | } 18 | 19 | // Get the contents of the setting ui 20 | public static getContents(): any 21 | { 22 | return this.getContents_internal(); 23 | } 24 | 25 | /////////////////////////////////////////////////////////////////////////////////////////////////// 26 | 27 | private static _shortcutFileUis: any; 28 | private static _vaultFiles: Array; 29 | 30 | private static create_internal(parent: any, settings: any, app: any): void 31 | { 32 | // Refresh the file list (it may have changed since last time) 33 | this._vaultFiles = []; 34 | for (const key in app.vault.fileMap) 35 | { 36 | if (key.endsWith(".md")) 37 | { 38 | this._vaultFiles.push(key.slice(0, -3)); 39 | } 40 | } 41 | 42 | new Setting(parent) 43 | .setName("Shortcut-files") 44 | .setDesc("Addresses of notes containing shortcut-file content.") 45 | .addButton((button: any) => 46 | { 47 | return button 48 | .setButtonText("Add shortcut-file") 49 | .setClass("iscript_spacedUi") 50 | .onClick( () => this.addShortcutFileUi(app) ); 51 | }) 52 | .addButton((button: any) => 53 | { 54 | return button 55 | .setButtonText("Import full library") 56 | .setClass("iscript_spacedUi") 57 | .onClick(async () => 58 | { 59 | if (await LibraryImporter.run()) 60 | { 61 | document.getElementById("alert_libraryUpdates"). 62 | toggleClass("iscript_hidden", true); 63 | } 64 | }); 65 | }); 66 | this._shortcutFileUis = parent.createEl("div", { cls: "iscript_shortcutFiles" }); 67 | this._shortcutFileUis.createEl("div", { 68 | text: "Red means the file does not exist.", 69 | cls: "setting-item-description iscript_extraMessage iscript_onSiblings" 70 | }); 71 | 72 | // Add a filename ui item for each shortcut-file in settings 73 | for (const shortcutFile of settings.shortcutFiles) 74 | { 75 | this.addShortcutFileUi(app, shortcutFile); 76 | } 77 | } 78 | 79 | private static getContents_internal(): any 80 | { 81 | let result: Array = []; 82 | for (const shortcutFileUi of this._shortcutFileUis.childNodes) 83 | { 84 | if (shortcutFileUi.classList.contains("iscript_shortcutFile") && 85 | shortcutFileUi.childNodes[1].value) 86 | { 87 | result.push( 88 | { 89 | enabled: shortcutFileUi.childNodes[0].classList.contains("is-enabled"), 90 | address: normalizePath( shortcutFileUi.childNodes[1].value + ".md" ) 91 | }); 92 | } 93 | } 94 | return { shortcutFiles: result }; 95 | } 96 | 97 | private static addShortcutFileUi(app: any, shortcutFile?: any): void 98 | { 99 | let fileListUiId = "fileList" + this._shortcutFileUis.childNodes.length; 100 | let g: any = this._shortcutFileUis.createEl("div", { cls: "iscript_shortcutFile" }); 101 | let e: any = g.createEl("div", { cls: "checkbox-container iscript_toggle" }); 102 | e.toggleClass("is-enabled", shortcutFile ? shortcutFile.enabled : true); 103 | e.addEventListener("click", function(this: any) 104 | { 105 | this.classList.toggle("is-enabled"); 106 | }); 107 | e = g.createEl("input", { cls: "iscript_shortcutFileAddress" }); 108 | e.setAttr("type", "text"); 109 | e.setAttr("placeholder", "Filename"); 110 | e.setAttr("list", fileListUiId); 111 | e.settings = this; 112 | // Handle toggling red on this textfield 113 | e.addEventListener("input", function(this: any) 114 | { 115 | const isBadInput: boolean = 116 | this.value && !this.settings._vaultFiles.contains(this.value); 117 | this.toggleClass("iscript_badInput", isBadInput); 118 | }); 119 | // Assign given text argument to the textfield 120 | if (shortcutFile) 121 | { 122 | // Remove ".md" extension from filename 123 | e.setAttr("value", shortcutFile.address.slice(0, -3)); 124 | } 125 | e.dispatchEvent(new Event("input")); 126 | e = g.createEl("datalist"); 127 | e.id = fileListUiId; 128 | for (const file of this._vaultFiles) 129 | { 130 | e.createEl("option").value = file; 131 | } 132 | e = g.createEl("button", { cls: "iscript_upButton iscript_spacedUi" }); 133 | e.group = g; 134 | e.onclick = SettingUi_Helper.upButtonClicked; 135 | e.listOffset = 1; 136 | e = g.createEl("button", { cls: "iscript_downButton iscript_spacedUi" }); 137 | e.group = g; 138 | e.onclick = SettingUi_Helper.downButtonClicked; 139 | e = g.createEl("button", { cls: "iscript_deleteButton iscript_spacedUi" }); 140 | e.group = g; 141 | e.onclick = SettingUi_Helper.deleteButtonClicked; 142 | e.app = app; 143 | e.typeTitle = "shortcut-file"; 144 | }; 145 | } 146 | -------------------------------------------------------------------------------- /src/ui_setting_shortcutFormat.ts: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // Setting ui shortcut format - Create and work with a setting for the format of shortcut input. // 3 | /////////////////////////////////////////////////////////////////////////////////////////////////// 4 | 5 | "use strict"; 6 | 7 | import { Setting, Platform } from "obsidian"; 8 | 9 | export abstract class SettingUi_ShortcutFormat 10 | { 11 | // Create the setting ui 12 | public static create(parent: any, settings: any): void 13 | { 14 | return this.create_internal(parent, settings); 15 | } 16 | 17 | // Get the contents of the setting ui 18 | public static getContents(): any 19 | { 20 | return this._settings; 21 | } 22 | 23 | // Called before recording settings changes 24 | public static finalize(): void 25 | { 26 | this.finalize_internal(); 27 | } 28 | 29 | /////////////////////////////////////////////////////////////////////////////////////////////////// 30 | 31 | private static _settings: any; 32 | private static _originalSettings: any; 33 | private static _formatErrMsgContainerUi: any; 34 | private static _formatErrMsgContentUi: any; 35 | private static _shortcutExampleUi: any; 36 | 37 | private static create_internal(parent: any, settings: any): void 38 | { 39 | this._settings = { prefix: settings.prefix, suffix: settings.suffix }; 40 | this._originalSettings = { prefix: settings.prefix, suffix: settings.suffix }; 41 | 42 | parent.createEl("h2", { text: "Shortcut format" }); 43 | 44 | // A ui for showing errors in shortcut format settings 45 | this._formatErrMsgContainerUi = parent.createEl("div", { cls: "iscript_alert iscript_hidden" }); 46 | this._formatErrMsgContainerUi.createEl("span", { text: "ERROR", cls: "iscript_errMsgTitle" }); 47 | this._formatErrMsgContentUi = this._formatErrMsgContainerUi.createEl("span"); 48 | 49 | // Prefix 50 | new Setting(parent) 51 | .setName("Shortcut prefix") 52 | .setDesc("What to type BEFORE a shortcut.") 53 | .addText((text: any) => 54 | { 55 | return text 56 | .setPlaceholder("Prefix") 57 | .setValue(settings.prefix) 58 | .onChange((value: string) => 59 | { 60 | this._settings.prefix = value; 61 | this.refreshShortcutExample(); 62 | this.isFormatValid(); 63 | }); 64 | }) 65 | .settingEl.toggleClass("iscript_settingBundledTop", !Platform.isMobile); 66 | 67 | // Suffix 68 | new Setting(parent) 69 | .setName("Shortcut suffix") 70 | .setDesc("What to type AFTER a shortcut.") 71 | .addText((text: any) => 72 | { 73 | return text 74 | .setPlaceholder("Suffix") 75 | .setValue(settings.suffix) 76 | .onChange((value: string) => 77 | { 78 | this._settings.suffix = value; 79 | this.refreshShortcutExample(); 80 | this.isFormatValid(); 81 | }); 82 | }) 83 | .settingEl.toggleClass("iscript_settingBundled", !Platform.isMobile); 84 | 85 | // Example 86 | const exampleOuter: any = parent.createEl("div", { cls: "setting-item" }); 87 | exampleOuter.toggleClass("iscript_settingBundled", !Platform.isMobile); 88 | const exampleInfo: any = exampleOuter.createEl("div", { cls: "setting-item-info" }); 89 | exampleInfo.createEl("div", { text: "Example", cls: "setting-item-name" }); 90 | exampleInfo.createEl("div", 91 | { 92 | text: "How to write the shortcut \"D100\"", 93 | cls: "setting-item-description" 94 | }); 95 | const exampleItemControl: any = 96 | exampleOuter.createEl("div", { cls: "setting-item-control" }); 97 | this._shortcutExampleUi = exampleItemControl.createEl("div", { cls: "iscript_labelControl" }); 98 | 99 | // Finish by filling example 100 | this.refreshShortcutExample(); 101 | } 102 | 103 | private static finalize_internal(): void 104 | { 105 | if (!this.isFormatValid()) 106 | { 107 | this._settings.prefix = this._originalSettings.prefix; 108 | this._settings.suffix = this._originalSettings.suffix; 109 | } 110 | } 111 | 112 | // Checks format settings for errors: 113 | // - blank prefix or suffix 114 | // - suffix contains prefix (disallowed as it messes up logic) 115 | private static isFormatValid(): boolean 116 | { 117 | let err: string = ""; 118 | if (!this._settings.prefix) 119 | { 120 | err = "Prefix cannot be blank"; 121 | } 122 | else if (!this._settings.suffix) 123 | { 124 | err = "Suffix cannot be blank"; 125 | } 126 | else if (this._settings.suffix.includes(this._settings.prefix)) 127 | { 128 | err = "Suffix cannot contain prefix"; 129 | } 130 | else if (this._settings.prefix.match(/\*|\(|\_|\{|\[|\'|\"|\`/) || 131 | this._settings.suffix.match(/\*|\(|\_|\{|\[|\'|\"|\`/)) 132 | { 133 | err = "Prefix and suffix cannot contain characters with auto-complete: * ( _ { [ ' \" `"; 134 | } 135 | 136 | if (!err) 137 | { 138 | this._formatErrMsgContainerUi.toggleClass( 139 | "iscript_hidden", true); 140 | return true; 141 | } 142 | else 143 | { 144 | this._formatErrMsgContainerUi.toggleClass( 145 | "iscript_hidden", false); 146 | this._formatErrMsgContentUi.innerText = err; 147 | return false; 148 | } 149 | } 150 | 151 | private static refreshShortcutExample(): void 152 | { 153 | this._shortcutExampleUi.innerText = this._settings.prefix + "D100" + this._settings.suffix; 154 | }; 155 | } 156 | -------------------------------------------------------------------------------- /src/ui_setting_shortcuts.ts: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////////// 2 | // Setting ui shortcuts - Create and work with a setting of a list of shortcuts. // 3 | /////////////////////////////////////////////////////////////////////////////////// 4 | 5 | "use strict"; 6 | 7 | import { Setting } from "obsidian"; 8 | import { SettingUi_Helper } from "./ui_setting_helper"; 9 | import { ShortcutLoader } from "./ShortcutLoader"; 10 | import { DEFAULT_SETTINGS } from "./defaultSettings"; 11 | 12 | export abstract class SettingUi_Shortcuts 13 | { 14 | // Create the setting ui 15 | public static create(parent: any, settings: any, app: any): void 16 | { 17 | return this.create_internal(parent, settings, app); 18 | } 19 | 20 | // Get the contents of the setting ui 21 | public static getContents(): any 22 | { 23 | return this.getContents_internal(); 24 | } 25 | 26 | /////////////////////////////////////////////////////////////////////////////////////////////////// 27 | 28 | private static _shortcutUis: any; 29 | 30 | private static create_internal(parent: any, settings: any, app: any): void 31 | { 32 | new Setting(parent) 33 | .setName("Shortcuts") 34 | .setDesc("Define shortcuts here (in addition to shortcut-files).") 35 | .addButton((button: any) => 36 | { 37 | return button 38 | .setButtonText("Add shortcut") 39 | .setClass("iscript_spacedUi") 40 | .onClick( () => this.addShortcutUi(app) ); 41 | }) 42 | .addButton((button: any) => 43 | { 44 | return button 45 | .setButtonText("Add defaults") 46 | .setClass("iscript_spacedUi") 47 | .onClick(( () => 48 | { 49 | let defaultShortcuts: Array = ShortcutLoader.parseShortcutFile( 50 | "Settings", DEFAULT_SETTINGS.shortcuts, true, true).shortcuts; 51 | 52 | // We don't want to duplicate shortcuts, and it's important to keep 53 | // defaults in-order. Remove any shortcuts from the ui list that are part 54 | // of the defaults before adding the defaults to the end of the ui list. 55 | this.removeShortcutsFromUi(defaultShortcuts); 56 | 57 | for (const defaultShortcut of defaultShortcuts) 58 | { 59 | this.addShortcutUi(app, defaultShortcut); 60 | } 61 | } ).bind(this)); 62 | }); 63 | this._shortcutUis = parent.createEl("div", { cls: "iscript_shortcuts" }); 64 | 65 | // Add a shortcut ui item for each shortcut in settings 66 | const shortcuts: Array = ShortcutLoader.parseShortcutFile( 67 | "Settings", settings.shortcuts, true, true).shortcuts; 68 | for (const shortcut of shortcuts) 69 | { 70 | this.addShortcutUi(app, shortcut); 71 | } 72 | } 73 | 74 | private static getContents_internal(): any 75 | { 76 | let result: string = ""; 77 | for (const shortcutUi of this._shortcutUis.childNodes) 78 | { 79 | // Only accept shortcuts with a non-empty Expansion string 80 | if (!shortcutUi.childNodes[4].value) { continue; } 81 | 82 | result += 83 | "\n__\n" + shortcutUi.childNodes[0].value + 84 | "\n__\n" + shortcutUi.childNodes[4].value + 85 | "\n__\n" + shortcutUi.childNodes[5].value + "\n"; 86 | } 87 | 88 | return { shortcuts: result }; 89 | } 90 | 91 | private static addShortcutUi(app: any, shortcut?: any): void 92 | { 93 | let g: any = this._shortcutUis.createEl("div", { cls: "iscript_shortcut" }); 94 | let e: any = g.createEl("input", { cls: "iscript_shortcutTest" }); 95 | e.setAttr("type", "text"); 96 | e.setAttr("placeholder", "Test (regex)"); 97 | if (shortcut) 98 | { 99 | e.value = shortcut.test.source; 100 | 101 | // Translate regex compiled "blank" test 102 | if (e.value === "(?:)") 103 | { 104 | e.value = ""; 105 | } 106 | } 107 | e = g.createEl("button", { cls: "iscript_upButton iscript_spacedUi" }); 108 | e.group = g; 109 | e.onclick = SettingUi_Helper.upButtonClicked; 110 | e.listOffset = 0; 111 | e = g.createEl("button", { cls: "iscript_downButton iscript_spacedUi" }); 112 | e.group = g; 113 | e.onclick = SettingUi_Helper.downButtonClicked; 114 | e = g.createEl("button", { cls: "iscript_deleteButton iscript_spacedUi" }); 115 | e.group = g; 116 | e.onclick = SettingUi_Helper.deleteButtonClicked; 117 | e.app = app; 118 | e.typeTitle = "shortcut"; 119 | e = g.createEl("textarea", { cls: "iscript_shortcutExpansion" }); 120 | e.setAttr("placeholder", "Expansion (JavaScript)"); 121 | if (shortcut) 122 | { 123 | e.value = shortcut.expansion; 124 | } 125 | e = g.createEl("textarea", { cls: "iscript_shortcutAbout" }); 126 | e.setAttr("placeholder", "About (text)"); 127 | if (shortcut) 128 | { 129 | e.value = shortcut.about; 130 | } 131 | }; 132 | 133 | // Takes a list of shortcuts, and removes them from the ui, if they are there. 134 | private static removeShortcutsFromUi(shortcuts: any): void 135 | { 136 | let toRemove: Array = []; 137 | for (const shortcutUi of this._shortcutUis.childNodes) 138 | { 139 | const test: string = shortcutUi.childNodes[0].value; 140 | const expansion: string = shortcutUi.childNodes[4].value; 141 | for (let k: number = 0; k < shortcuts.length; k++) 142 | { 143 | if (shortcuts[k].expansion !== expansion) { continue; } 144 | if (shortcuts[k].test.source !== test && 145 | (shortcuts[k].test.source !== "(?:)" || test !== "")) 146 | { 147 | continue; 148 | } 149 | toRemove.push(shortcutUi); 150 | break; 151 | } 152 | } 153 | for (const shortcutUi of toRemove) 154 | { 155 | shortcutUi.remove(); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/ui_settings.ts: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////// 2 | // Settings - the settings ui and logic for this plugin project // 3 | ////////////////////////////////////////////////////////////////// 4 | 5 | "use strict"; 6 | 7 | import { PluginSettingTab } from "obsidian"; 8 | import InlineScriptsPlugin from "./_Plugin"; 9 | import { SettingUi_ShortcutFiles } from "./ui_setting_shortcutFiles"; 10 | import { SettingUi_Shortcuts } from "./ui_setting_shortcuts"; 11 | import { SettingUi_ShortcutFormat } from "./ui_setting_shortcutFormat"; 12 | import { SettingUi_ExpansionFormat } from "./ui_setting_expansionFormat"; 13 | import { SettingUi_Other } from "./ui_setting_other"; 14 | import { SettingUi_Actions } from "./ui_setting_actions"; 15 | import { SettingUi_Alerts } from "./ui_setting_alerts"; 16 | import { ShortcutLoader } from "./ShortcutLoader"; 17 | import { DfcMonitorType } from "./Dfc"; 18 | 19 | export class InlineScriptsPluginSettings extends PluginSettingTab 20 | { 21 | public constructor(plugin: InlineScriptsPlugin) 22 | { 23 | super(plugin.app, plugin); 24 | } 25 | 26 | public display(): void 27 | { 28 | this.display_internal(); 29 | } 30 | 31 | public hide(): void 32 | { 33 | this.hide_internal(); 34 | } 35 | 36 | /////////////////////////////////////////////////////////////////////////////////////////////////// 37 | 38 | // Members of PluginSettingTab not included in it's type definition 39 | private plugin: InlineScriptsPlugin; 40 | 41 | private display_internal(): void 42 | { 43 | const c: any = this.containerEl; 44 | c.empty(); 45 | 46 | // App version (in header) 47 | c.createEl("div", { text: this.plugin.manifest.version, cls: "iscript_version" }); 48 | 49 | // Updates available alerts 50 | SettingUi_Alerts.create(c); 51 | 52 | // Action buttons 53 | SettingUi_Actions.create(c); 54 | 55 | // Heading for the next few sections 56 | c.createEl("h2", { text: "Shortcut Sources" }); 57 | 58 | // Shortcut-files setting 59 | SettingUi_ShortcutFiles.create(c, this.plugin.settings, this.plugin.app); 60 | 61 | // Shortcuts setting 62 | SettingUi_Shortcuts.create(c, this.plugin.settings, this.plugin.app); 63 | 64 | // Shortcut format settings 65 | SettingUi_ShortcutFormat.create(c, this.plugin.settings); 66 | 67 | // Expansion format settings 68 | SettingUi_ExpansionFormat.create(c, this.plugin.settings); 69 | 70 | // Other settings 71 | SettingUi_Other.create(c, this.plugin.settings); 72 | 73 | // App version (in footer) 74 | c.createEl("div", { text: this.plugin.manifest.version, cls: "iscript_version" }); 75 | } 76 | 77 | // THIS is where settings are saved! 78 | private hide_internal(): void 79 | { 80 | let newSettings: any = {}; 81 | 82 | // Plugin version 83 | newSettings.version = this.plugin.manifest.version; 84 | 85 | // Get the shortcut format settings 86 | SettingUi_ShortcutFormat.finalize(); 87 | Object.assign(newSettings, SettingUi_ShortcutFormat.getContents()); 88 | 89 | // Get the expansion format settings 90 | Object.assign(newSettings, SettingUi_ExpansionFormat.getContents()); 91 | 92 | // Get the other settings 93 | Object.assign(newSettings, SettingUi_Other.getContents()); 94 | 95 | // Get shortcut-files list 96 | Object.assign(newSettings, SettingUi_ShortcutFiles.getContents()); 97 | 98 | // Get shortcuts list 99 | Object.assign(newSettings, SettingUi_Shortcuts.getContents()); 100 | 101 | // Button settings 102 | newSettings.buttonView = this.plugin.settings.buttonView; 103 | 104 | ///////////////////////////////////////////////////// 105 | // Determine if shortcuts setting was changed. // 106 | // If so, set "forceRefreshShortcuts" for the Dfc. // 107 | ///////////////////////////////////////////////////// 108 | const oldShortcuts: any = ShortcutLoader.parseShortcutFile( 109 | "", this.plugin.settings.shortcuts, true, true).shortcuts; 110 | const newShortcuts: any = ShortcutLoader.parseShortcutFile( 111 | "", newSettings.shortcuts, true, true).shortcuts; 112 | // Start by comparing list counts 113 | let forceRefreshShortcuts: boolean = (newShortcuts.length !== oldShortcuts.length); 114 | // If old & new shortcut settings have the same list count, check each individual shortcut 115 | // for a change between old and new 116 | if (!forceRefreshShortcuts) 117 | { 118 | for (let i: number = 0; i < newShortcuts.length; i++) 119 | { 120 | if (newShortcuts[i].test.source !== oldShortcuts[i].test.source || 121 | newShortcuts[i].expansion !== oldShortcuts[i].expansion || 122 | newShortcuts[i].about !== oldShortcuts[i].about) 123 | { 124 | forceRefreshShortcuts = true; 125 | break; 126 | } 127 | } 128 | } 129 | 130 | // Replace old settings with new 131 | this.plugin.settings = newSettings; 132 | 133 | // Update the Dfc monitor-mode based on devmode setting. 134 | this.plugin.shortcutDfc.setMonitorType( 135 | this.plugin.settings.devMode ? DfcMonitorType.OnTouch : DfcMonitorType.OnModify); 136 | 137 | // Update the Dfc file-list based on list of shortcut files. 138 | this.plugin.shortcutDfc.updateFileList( 139 | this.plugin.getActiveShortcutFileAddresses(), forceRefreshShortcuts); 140 | 141 | // Update variable for the suffix's final character 142 | this.plugin.suffixEndCharacter = 143 | this.plugin.settings.suffix.charAt(this.plugin.settings.suffix.length - 1); 144 | 145 | // Store the settings to file 146 | this.plugin.saveSettings(); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/ui_userNotifier.ts: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////////////////////// 2 | // UserNotifier - wrapper around sending messages to user via console and popup notifications // 3 | //////////////////////////////////////////////////////////////////////////////////////////////// 4 | 5 | "use strict"; 6 | 7 | import { Notice } from "obsidian"; 8 | import InlineScriptsPlugin from "./_Plugin"; 9 | 10 | // parameters for "run" (all optional): 11 | // - popupMessage: string 12 | // - Text displayed in popup notification. Falls back to "message", then to no popup displayed. 13 | // - ConsoleMessage: string 14 | // - Text displayed in console. Falls back to "message", then no console output. 15 | // - message: string 16 | // - Text displayed in popup (popupMessage overrides) and console (consoleMessage overrides). 17 | // - messageType: string 18 | // - Text representing message category. Example: "EXPANSION-ERROR". Shown in console output. 19 | // - messageLevel 20 | // - Determines function used for console output. 21 | // - If "info", use console.info(). If "warn", use console.warn(). Else, use console.error(). 22 | // - consoleHasDetails: boolean 23 | // - If true, popup notification includes a suggestion to review console output for details. 24 | // - popupTime: float 25 | // - a multiplier for the amount of time that a popup stays on the screen. Defaults to 1.0. 26 | 27 | const LONG_NOTE_TIME: number = 8 * 1000; 28 | const INDENT: string = " ".repeat(4); 29 | 30 | export namespace UserNotifier 31 | { 32 | // Creates a message to the user in a popup notification and/or a console log. 33 | // Takes an object of optional parameters. See this file's header for a parameter reference. 34 | export function run(parameters: any): void 35 | { 36 | run_internal(parameters); 37 | } 38 | 39 | // Offer "print" function for use by user-written shortcuts. 40 | // Function gives a message to the user in a popup notification and a console log. 41 | export function getFunction_print(): Function 42 | { 43 | return print; 44 | } 45 | 46 | /////////////////////////////////////////////////////////////////////////////////////////////////// 47 | 48 | function print(message: any): any 49 | { 50 | // Send the message to user as a popup notification and a console log. 51 | new Notice("Inline Script Shortcut:\n" + message, LONG_NOTE_TIME); 52 | console.info("Inline Script Shortcut:\n\t" + message); 53 | return message; 54 | }; 55 | 56 | function run_internal(parameters: any): void 57 | { 58 | // Make sure parameters is an object, so the logic for reading it isn't broken. 59 | if (typeof parameters !== "object") 60 | { 61 | parameters = {}; 62 | } 63 | 64 | // Message parameters 65 | const popupMessage: string = parameters.popupMessage || parameters.message || ""; 66 | const consoleMessage: string = 67 | (parameters.consoleMessage || parameters.message || "").replaceAll("\n", "\n" + INDENT); 68 | 69 | // Message type and level parameters 70 | const messageType: string = parameters.messageType || ""; 71 | const messageLevel: number = 72 | (parameters.messageLevel === "info") ? 0 : 73 | (parameters.messageLevel === "warn") ? 1 : 74 | 2; 75 | 76 | // Console detail parameter 77 | const consoleHasDetails = !!parameters.consoleHasDetails; 78 | 79 | // Popup time 80 | const popupTime = LONG_NOTE_TIME * (parameters.popupTime ?? 1.0); 81 | 82 | // Add the popup notification 83 | if (popupMessage) 84 | { 85 | new Notice( 86 | (messageLevel === 2 ? "ERROR: " : "") + 87 | popupMessage + 88 | (consoleHasDetails ? "\n\n(see console for details)" : ""), 89 | popupTime); 90 | } 91 | 92 | // Add the console log 93 | if (consoleMessage) 94 | { 95 | const message = 96 | InlineScriptsPlugin.getInstance().manifest.name + "\n" + 97 | (messageType ? (INDENT + messageType + "\n") : "") + 98 | INDENT + consoleMessage; 99 | switch (messageLevel) 100 | { 101 | case 0: console.info (message); break; 102 | case 1: console.warn (message); break; 103 | default: console.error(message); break; 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .iscript_extraMessage 2 | { 3 | text-align: left; 4 | width: 90%; 5 | display: inline-block; 6 | margin-bottom: 0.5em; 7 | font-style: italic; 8 | } 9 | .is-mobile .iscript_extraMessage 10 | { 11 | margin-top: 1em; 12 | } 13 | .iscript_onSiblings:only-child 14 | { 15 | display: none; 16 | } 17 | .iscript_shortcutFiles 18 | { 19 | margin-bottom: 1em; 20 | text-align: right; 21 | margin-top: -1em; 22 | } 23 | .iscript_shortcuts 24 | { 25 | margin-bottom: 1em; 26 | text-align: right; 27 | } 28 | .iscript_shortcutFile 29 | { 30 | margin-bottom: 1em; 31 | } 32 | .iscript_shortcutFileAddress 33 | { 34 | width: calc(90% - 9em); 35 | display: inline !important; 36 | } 37 | .is-mobile .iscript_shortcutFileAddress 38 | { 39 | width: calc(90% - 6.6em) !important; 40 | } 41 | 42 | .iscript_badInput 43 | { 44 | background-color: darkred !important; 45 | } 46 | .iscript_shortcut 47 | { 48 | margin-bottom: 2em; 49 | } 50 | .iscript_shortcutTest 51 | { 52 | width: calc(90% - 5.9em); 53 | display: inline !important; 54 | } 55 | .is-mobile .iscript_shortcutTest 56 | { 57 | width: calc(90% - 6.6em) !important; 58 | } 59 | .iscript_shortcutExpansion, .iscript_shortcutAbout 60 | { 61 | width: 90%; 62 | margin-top: 0.2em; 63 | background: var(--background-modifier-form-field); 64 | height: 6em; 65 | } 66 | .iscript_shortcutAbout 67 | { 68 | height: 4.5em; 69 | } 70 | .iscript_deleteButton, .iscript_upButton, .iscript_downButton 71 | { 72 | margin: 0; 73 | margin-left: 0.2em; 74 | padding: 0.5em; 75 | width: auto !important; 76 | } 77 | .iscript_deleteButton:before 78 | { 79 | content: "🗑"; 80 | display: inline; 81 | } 82 | .iscript_upButton:before 83 | { 84 | content: "▲"; 85 | display: inline; 86 | } 87 | .iscript_downButton:before 88 | { 89 | content: "▼"; 90 | display: inline; 91 | } 92 | .iscript_errMsgTitle 93 | { 94 | font-weight: bold; 95 | margin-right: 1em; 96 | } 97 | .iscript_settingBundled 98 | { 99 | border: 0; 100 | padding: 0; 101 | padding-top: 1em; 102 | } 103 | .iscript_settingBundledTop 104 | { 105 | padding-bottom: 0; 106 | } 107 | .iscript_labelControl 108 | { 109 | border: 1px solid var(--background-modifier-border); 110 | color: var(--text-normal); 111 | font-family: inherit; 112 | padding: 5px 14px; 113 | font-size: 16px; 114 | border-radius: 4px; 115 | outline: none; 116 | height: 30px; 117 | width: 10em; 118 | text-align: left; 119 | } 120 | .is-mobile .iscript_labelControl 121 | { 122 | width: 100%; 123 | } 124 | .iscript_alert 125 | { 126 | background-color: red; 127 | color: white; 128 | border-radius: 1em; 129 | padding: 0em 1em; 130 | } 131 | #iscript_inputBlocker 132 | { 133 | position: absolute; 134 | left: 0; 135 | right: 0; 136 | top: 0; 137 | bottom: 0; 138 | background-color: black; 139 | opacity: 0.75; 140 | z-index: 999; 141 | transition: opacity 1s; 142 | } 143 | .iscript_version 144 | { 145 | text-align: right; 146 | } 147 | .iscript_version + h2 148 | { 149 | margin-top: 0; 150 | } 151 | .iscript_spacedUi 152 | { 153 | margin-left: .25em !important; 154 | } 155 | .iscript_toggle 156 | { 157 | margin-right: 0.5em; 158 | margin-bottom: -0.3em; 159 | } 160 | .iscript_popup input, .iscript_popup select 161 | { 162 | width: 100%; 163 | max-width: unset !important; 164 | } 165 | .iscript_popup 166 | { 167 | padding: 1em; 168 | padding-top: 2.5em; 169 | } 170 | .iscript_popup .setting-item 171 | { 172 | border-top: none; 173 | } 174 | .iscript_suggestionDescription 175 | { 176 | background-color: #16172b; 177 | position: absolute; 178 | padding: 0.75em; 179 | border-radius: 4px; 180 | border: 1px solid var(--background-modifier-border); 181 | width: auto !important; 182 | height: auto !important; 183 | left: 2em; 184 | right: 2em; 185 | bottom: 1em; 186 | display: none; 187 | z-index: 100; 188 | } 189 | .iscript_suggestionDescription_above 190 | { 191 | bottom: unset; 192 | top: 3em; 193 | } 194 | .iscript_suggestionHighlight 195 | { 196 | font-weight: bold; 197 | text-decoration: underline; 198 | } 199 | .iscript_shortcutFileAddress::-webkit-calendar-picker-indicator 200 | { 201 | position: relative; 202 | top: -.25em; 203 | right: -.5em; 204 | } 205 | .iscript_preFadein 206 | { 207 | opacity: 0.0 !important; 208 | } 209 | .iscript_messageCount 210 | { 211 | position: absolute; 212 | top: 0.25em; 213 | font-size: 75%; 214 | } 215 | .iscript_actionsSection 216 | { 217 | text-align: center; 218 | padding: 1em 0em; 219 | border-top: 1px solid var(--background-modifier-border); 220 | } 221 | .iscript_button_disabled 222 | { 223 | color: var(--text-faint); 224 | cursor: unset 225 | } 226 | .iscript_button_disabled:hover 227 | { 228 | background-color: var(--interactive-normal); 229 | box-shadow: var(--input-shadow); 230 | } 231 | .iscript_buttonView_groupSelect 232 | { 233 | width: 100% 234 | } 235 | .iscript_buttonView_hr 236 | { 237 | margin: 0; 238 | margin-top: 0.5em; 239 | } 240 | .iscript_buttonView_shortcutButton 241 | { 242 | margin-right: 0.25em; 243 | margin-bottom: 1em; 244 | } 245 | .iscript_buttonView_uiButton 246 | { 247 | margin-top: 0.5em; 248 | padding: .25em; 249 | padding-bottom: 0; 250 | background-color: unset !important; 251 | } 252 | .iscript_buttonView_uiButton_selected 253 | { 254 | background-color: var(--text-highlight-bg) !important; 255 | border-radius: 4px; 256 | } 257 | .iscript_buttonView_uiBlock 258 | { 259 | font-style: italic; 260 | margin-top: 0.7em; 261 | margin-left: 0.5em; 262 | } 263 | .iscript_buttonView_help 264 | { 265 | margin-bottom: 0.5em; 266 | border: solid; 267 | border-color: var(--text-highlight-bg); 268 | border-radius: 4px; 269 | padding: 0.25em 0.5em; 270 | margin-top: 0.15em; 271 | display: none; 272 | } 273 | .widget-icon 274 | { 275 | width: 20px; 276 | height: 20px; 277 | fill: var(--text-muted); 278 | } 279 | .iscript_hidden 280 | { 281 | display: none; 282 | } 283 | .iscript_donationsCaption 284 | { 285 | text-align: center; 286 | margin: auto; 287 | } 288 | .iscript_donations 289 | { 290 | text-align: center; 291 | margin: 1em; 292 | } 293 | .iscript_donations > a 294 | { 295 | text-decoration: none; 296 | } 297 | 298 | .iscript_drag_container 299 | { 300 | touch-action: none; 301 | } 302 | .iscript_drag_item 303 | { 304 | touch-action: none; 305 | cursor: grab; 306 | } 307 | .iscript_drag_container_dragging 308 | { 309 | cursor: grabbing; 310 | } 311 | .iscript_drag_item_dragged 312 | { 313 | border: 2px solid yellow; 314 | cursor: unset !important; 315 | } 316 | .iscript_drag_item_notDragged 317 | { 318 | filter: brightness(.75); 319 | cursor: unset !important; 320 | } 321 | .iscript_listSelect 322 | { 323 | background-image: none !important; 324 | padding-right: 0.75em; 325 | height: unset; 326 | padding-top: 0.5em; 327 | padding-bottom: 0.5em; 328 | } 329 | .iscript_listSelect option:not(:checked) 330 | { 331 | background-color: rgb(0,0,0,0); 332 | } 333 | .iscript_checkbox 334 | { 335 | height: 16px; 336 | width: 16px !important; 337 | } -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | ### This release (modifications) 2 | - add input-block removal when shortcut failure would cause skipping input-block removal 3 | - have Inline Scripts accept  . (convert to normal space internally). Put   back into readme tables 4 | 5 | ### Next release - Critical 6 | - look into db-folder compatibility 7 | - __notepick pickFromQuery {count: >0} {pick id: name text, default: ""} : {query: text}__ - Picks {count} random notes based on {query} and remembers them as {pick id}. The query can contain: 8 | - __path:__ - Allows entering a path (complete or partial) that file choices will be filtered down to 9 | - __file:__ - Allows entering a filename (complete or partial) that file choices will be filtered down to 10 | - __tags:__ - Allows entering a tag that file choices will be filtered down to 11 | - try and resolve delay issues with notevars set 12 | 13 | ### Next release 14 | - file create {file-path: path text} {template-path: path text, default: none} 15 | - file modify "{start: regex string}" "{end: regex string}" {action: >=0, 0=replace, 1=append, 2=prepend} {content: text} 16 | - sublists sfile 17 | - location crafter sfile 18 | - Look into adding "state save " and "state load " shortcuts 19 | - adventurecrafter split into non-ui and ui shortcut variations 20 | - document all undocumented features - go through all shortcuts and note anything used, but undocumented (list here) then document it 21 | - add to buttons panel to toggle printing results to the note or printing to a popup 22 | - finish mythicv2 (missing stats system) 23 | - have shortcut that expands all unexpanded shortcuts in the note. 24 | - fix specific situation in autcomplete: 25 | - path texts - "files extensionchange" doesn't work right 26 | - x OR y OR z (should explicitly check if text meets each value) 27 | - add customizable parameter types to allow for things like "list name text" which would add an autocomplete for list names 28 | - bug fixes 29 | - pick ui starts at bottom 30 | - pick ui should have bottom buttons outside of scrolling (always on screen) 31 | - Go through sfiles before lists and look for places where I can use shortcut embedding (now that expUnformat is available) 32 | - plugin - replace all '==' with '===' and '!=' with '!==' where appropriate 33 | 34 | ### Post-next release 35 | - test for bug with notepick https://github.com/jon-heard/obsidian-inline-scripts/discussions/40 36 | - check for issues with "QuickAdd" plugin: https://www.reddit.com/r/solorpgplay/comments/y41sbj/comment/itd962a/?context=3 37 | - figure out how to determine if there's not a lot of screen resolution, and hide autocomplete and ac help panel if so 38 | - Have shortcut autocomplete react to ALL parameter types (when they are provided by the syntax string) 39 | - allow creating custom types (such as lists, to allow for a special autocomplete for a list name) 40 | - library - write an sfile for ironsworn (or D&D) 41 | - Shift from beta to release (after making either a D&D system or Ironsworn system) 42 | - plugin - typescript - use HTMLElement type wherever possible 43 | - Find a way to allow the prefix & suffix to contain auto-closed characters - `{`, `"`, etc 44 | --------------------------------------------------------------------------------