yourjs-box 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Embed an interactive JavaScript console on any web page with one script tag.
4
4
  Step-by-step code blocks, DevTools-style output, and code that runs in a Web
5
- Worker or the page itself.
5
+ Worker or the page itself. TypeScript works too.
6
6
 
7
7
  **[Live demo](https://westc.github.io/yourjs-box/)** ·
8
8
  [Examples](https://westc.github.io/yourjs-box/examples/)
@@ -61,10 +61,10 @@ If the editor already has code in it you are asked before it is replaced.
61
61
  history.
62
62
  - The **⋯** menu has:
63
63
  - **Text size**, which is remembered for every console on the same site.
64
- - **Open**, which loads a JavaScript file as the code that the console starts
64
+ - **Open**, which loads a JavaScript (or TypeScript) file as the code that the console starts
65
65
  with (its hidden blocks are hidden and Reset goes back to it).
66
66
  - **Save**, which saves the code that already ran and the code in the editor
67
- as a JavaScript file that can be opened later.
67
+ as a JavaScript (or TypeScript) file that can be opened later.
68
68
  - **Pop out into a window**, which moves the console into a separate window
69
69
  while the code keeps running in the page (so in window mode the code can
70
70
  still change the page). Close the window or click **Bring it back** to
@@ -99,9 +99,10 @@ objects and arrays) for a menu with:
99
99
  | Attribute | Description |
100
100
  | --- | --- |
101
101
  | `data-block-type` | `"classic"` (default) runs each block like a regular `<script>` so top-level declarations are shared between blocks. `"module"` runs each block like a `<script type="module">`. See below. |
102
+ | `data-language` | `"javascript"` (default) or `"typescript"`. See "TypeScript" below. |
102
103
  | `data-show-results` | `"false"` stops the value of the last expression in each block from being shown. |
103
104
  | `data-imports-url` | Where packages imported by name are loaded from. See "Importing Packages" below. |
104
- | `data-libraries-url` | Where to load Vue, Ace, Prism and Acorn from. See "Self-Hosting the Libraries" below. |
105
+ | `data-libraries-url` | Where to load Vue, Ace, Prism, Acorn and Babel from. See "Self-Hosting the Libraries" below. |
105
106
  | `data-runner` | `"worker"` (default) runs the code in a Web Worker. `"window"` runs it directly in the page. See below. |
106
107
  | `data-divider-orient` | `"vertical"` puts the editor beside the output and `"horizontal"` puts it below. If not specified the editor is beside the output unless the console is narrower than 600px. |
107
108
  | `data-hide-prefix` | Any block whose header starts with this prefix is hidden. See below. |
@@ -126,6 +127,36 @@ one block can be used in the next, but `await` can only be used inside of
126
127
  - Top-level declarations stay inside of their block, just like in a module.
127
128
  Use `globalThis` to share values between blocks.
128
129
 
130
+ ### TypeScript
131
+
132
+ Add `data-language="typescript"` to write the code in TypeScript:
133
+
134
+ ```html
135
+ <script src="https://cdn.jsdelivr.net/npm/yourjs-box@1/dist/yourjs-box.min.js" data-language="typescript">
136
+ // Describe a person \\
137
+ interface Person { name: string; born: number }
138
+ const people: Person[] = [{name: 'Ada', born: 1815}, {name: 'Grace', born: 1906}];
139
+
140
+ // Find the oldest \\
141
+ function oldest<T extends Person>(list: T[]): T {
142
+ return list.reduce((a, b) => a.born <= b.born ? a : b);
143
+ }
144
+ oldest(people)
145
+ </script>
146
+ ```
147
+
148
+ Before each block runs, [Babel](https://babeljs.io/) removes the types, which
149
+ is how most TypeScript tools run code without a build step. The types aren't
150
+ checked, so code with type errors still runs, just like it would in
151
+ JavaScript. Enums, namespaces and parameter properties all work, and errors
152
+ point to the lines and columns in the TypeScript code.
153
+
154
+ TypeScript works with both block types. Only type imports are removed (eg.
155
+ `import type {Options} from 'x'` or the `type Options` part of
156
+ `import {type Options, format} from 'x'`), so an import is never dropped just
157
+ because nothing uses it yet. Babel is only downloaded by consoles that use
158
+ TypeScript.
159
+
129
160
  ### Importing Packages
130
161
 
131
162
  npm packages can be imported by name. They are loaded from
@@ -158,7 +189,8 @@ To load packages from somewhere else set `data-imports-url` to a URL where
158
189
 
159
190
  The console's interface uses [Vue](https://vuejs.org/),
160
191
  [Ace](https://ace.c9.io/), [Prism](https://prismjs.com/) and
161
- [Acorn](https://github.com/acornjs/acorn), which are loaded from unpkg by
192
+ [Acorn](https://github.com/acornjs/acorn) (plus
193
+ [Babel](https://babeljs.io/) for TypeScript), which are loaded from unpkg by
162
194
  default. Exact versions are always used so that a new release of one of them
163
195
  can't change how the console works.
164
196
 
@@ -179,6 +211,9 @@ next to their main files):
179
211
 
180
212
  ```bash
181
213
  npm install vue@3.5.43 ace-builds@1.44.0 prismjs@1.30.0 prism-themes@1.9.0 acorn@8.18.0
214
+
215
+ # Only needed for TypeScript consoles
216
+ npm install @babel/standalone@7.29.9
182
217
  ```
183
218
 
184
219
  The About window lists the versions that each version of JS Box uses.
@@ -232,7 +267,7 @@ functions). The options are:
232
267
  | `placement` | Where the console goes: `"fill"` (default) replaces the target's contents, `"append"` and `"prepend"` add it inside of the target, `"replace"` replaces the target itself and `"before"` and `"after"` add it next to the target. |
233
268
  | `height` | The CSS height of the console (a number is treated as pixels). Defaults to `"100%"` so the console fills its container. It is never less than 150px. |
234
269
  | `code` | The code that the console starts with. |
235
- | `runner`, `blockType`, `showResults`, `hidePrefix`, `dividerOrient`, `theme`, `importsUrl`, `librariesUrl` | The same as the `data-*` attributes above. |
270
+ | `runner`, `blockType`, `language`, `showResults`, `hidePrefix`, `dividerOrient`, `theme`, `importsUrl`, `librariesUrl` | The same as the `data-*` attributes above. |
236
271
 
237
272
  `YourJSBox.version` is the version of JS Box that was loaded.
238
273
 
@@ -286,14 +321,6 @@ which cover every feature. Add `?build=standard` or `?build=min` to its URL to
286
321
  test `dist/yourjs-box.js` or `dist/yourjs-box.min.js` instead of
287
322
  `dist/yourjs-box.full.js`.
288
323
 
289
- ## Roadmap
290
-
291
- - Open button - Load a JS file from the filesystem.
292
- - Save button - Save the current inputs as a JS file that can be opened later.
293
- - Add `@timeout` annotation to the special comments that will allow you to input the amount of seconds to wait since the last call to a console logging function before automatically running the next comment segmented block.
294
- - Allow for TypeScript
295
- - Allow for CoffeeScript
296
-
297
324
  ## License
298
325
 
299
326
  Released under the [MIT License](LICENSE). You're free to use, modify and
@@ -1,4 +1,4 @@
1
- /*! yourjs-box v1.6.0 | (c) 2023-present Chris West | MIT License | https://github.com/westc/yourjs-box */
1
+ /*! yourjs-box v1.7.0 | (c) 2023-present Chris West | MIT License | https://github.com/westc/yourjs-box */
2
2
  (() => {
3
3
  /**
4
4
  * Viewer IFRAME's CSS code
@@ -9,12 +9,12 @@
9
9
  * Information about this package (eg. its version).
10
10
  * @type {{name: string, version: string, homepage: string, repoUrl: string, bugsUrl: string}}
11
11
  */
12
- const PACKAGE_INFO = {"name":"yourjs-box","version":"1.6.0","homepage":"https://westc.github.io/yourjs-box/","repoUrl":"https://github.com/westc/yourjs-box","bugsUrl":"https://github.com/westc/yourjs-box/issues"};
12
+ const PACKAGE_INFO = {"name":"yourjs-box","version":"1.7.0","homepage":"https://westc.github.io/yourjs-box/","repoUrl":"https://github.com/westc/yourjs-box","bugsUrl":"https://github.com/westc/yourjs-box/issues"};
13
13
  /**
14
14
  * Viewer IFRAME's HTML code
15
15
  * @type {string}
16
16
  */
17
- const VIEWER_IFRAME_HTML = "<div id=\"splash\" aria-label=\"Loading\"><div class=\"splash-content\"><span class=\"logo logo-large\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"splash-progress\"><div></div></div><div class=\"splash-message splash-slow\">Still loading&hellip;</div><div class=\"splash-message splash-error\">JS Box couldn&rsquo;t load. Please check your connection and reload the page.</div></div></div><div id=\"vueApp\" v-cloak><input type=\"file\" ref=\"fileInput\" class=\"file-input\" accept=\".js,.mjs,.cjs,.txt,text/javascript,text/plain\" tabindex=\"-1\" aria-hidden=\"true\" @change=\"onFileChosen\"><div id=\"main\" ref=\"main\" :class=\"mainElemClassNames\" :style=\"mainElemStyles\" @mousedown=\"onMainElemMouseDown\"><div id=\"displays\"><div ref=\"displaysScroller\" @scroll=\"onDisplaysScroll\"><template v-for=\"display in displays\"><div v-if=\"display.type === 'prism'\"><div v-if=\"display.isHidden\" class=\"code-header toggleable no-select\" @click=\"display.isCodeShown = !display.isCodeShown\" :title=\"display.isCodeShown ? 'Hide Code' : 'Show Code'\"><span :class=\"['arrow', 'expandable', display.isCodeShown ? 'expanded' : '']\"></span>{{ display.header || 'Hidden code' }}</div><div v-else-if=\"display.header\" class=\"code-header\">{{ display.header }}</div><div v-if=\"display.isCodeShown\" class=\"console-row code-row\"><span class=\"row-icon\"><icon name=\"chevron\"></icon></span><prism language=\"javascript\" :code=\"display.value\" :is-dark=\"theme === 'dark'\" match-braces></prism><button class=\"copy-to-editor-button\" title=\"Copy to editor\" @click=\"copyToEditor(display.value)\"><icon name=\"copyToEditor\"></icon></button></div></div><div v-if=\"display.type === 'log' &amp;&amp; !isInCollapsedGroup(display)\" :class=\"['console-row', 'log-' + display.key, display.groupId ? 'group-header' : '']\" :style=\"{'--depth': display.groupIds.length}\" :title=\"display.name\" @click=\"onLogRowClick(display, $event)\"><span v-if=\"display.key === 'error' || display.key === 'assert'\" class=\"row-icon\"><icon name=\"consoleError\"></icon></span><span v-else-if=\"display.key === 'warn'\" class=\"row-icon\"><icon name=\"consoleWarning\"></icon></span><span v-else-if=\"display.key === 'result'\" class=\"row-icon\"><icon name=\"result\"></icon></span><span v-else-if=\"display.groupId\" :class=\"['row-icon', 'arrow', 'expandable', display.isCollapsed ? '' : 'expanded']\"></span><div class=\"row-content\"><table v-if=\"display.table\" class=\"console-table\"><thead><tr><th v-for=\"header in display.table.headers\">{{ header }}</th></tr></thead><tbody><tr v-for=\"row in display.table.rows\"><td>{{ row.index }}</td><td v-for=\"cell in row.cells\"><template v-if=\"cell\"><span v-for=\"part in cell.parts\" :class=\"'t-' + part[0]\">{{ part[1] }}</span></template></td></tr></tbody></table><js-value v-for=\"(description, index) in display.descriptions\" :description=\"description\" :key-path=\"[]\" :path=\"[display.logId, index]\"></js-value><div v-if=\"display.stack\" class=\"trace-stack\">{{ display.stack }}</div></div></div><div v-if=\"display.type === 'notice'\" class=\"console-row notice\">{{ display.message }}</div><div v-if=\"display.type === 'error'\" class=\"console-row log-error\"><span class=\"row-icon\"><icon name=\"consoleError\"></icon></span><div class=\"row-content\">{{ display.message }}</div></div></template></div></div><div class=\"divider\" ref=\"mainDivider\"></div><div id=\"editor\"><ace-editor ref=\"editor\" v-model=\"jsCode\" language=\"javascript\" :theme=\"theme\" :font-size=\"Math.round(12 * textScale)\" @key-combo=\"onEditorKeyCombo\" @history-key=\"onEditorHistoryKey\" height=\"100%\"></ace-editor></div></div><div id=\"bottomNav\"><div class=\"brand\"><button class=\"logo-button\" title=\"About JS Box\" @click=\"openAbout('about')\"><span class=\"logo\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span></button> <span class=\"runner-badge\" :title=\"runnerMode === 'window' ? 'Code runs directly in this page' : 'Code runs in a Web Worker (no DOM access)'\">{{ runnerMode === 'window' ? 'Window' : 'Worker' }}</span></div><div class=\"buttons\"><template v-for=\"bottomButton in bottomButtons\"><span v-if=\"bottomButton.isSeparator\" class=\"separator\"></span> <button v-else :class=\"bottomButton.className\" @click=\"bottomButton.callback.call(this, $event)\" :title=\"bottomButton.title\" :disabled=\"bottomButton.disableIf &amp;&amp; bottomButton.disableIf.call(this)\"><icon :name=\"bottomButton.iconName\"></icon><span v-if=\"bottomButton.label\" class=\"label\">{{ bottomButton.label }}</span></button></template></div></div><div v-if=\"isMenuOpen\" class=\"more-menu\" role=\"menu\" aria-label=\"More\" ref=\"moreMenu\" @keydown=\"onMenuKeyDown\"><div class=\"menu-row\" role=\"group\" aria-label=\"Text size\"><span class=\"menu-row-label\">Text size</span> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Smaller text\" aria-label=\"Smaller text\" :disabled=\"!canShrinkText\" @click=\"changeTextScale(-1)\">A&minus;</button> <button class=\"menu-item menu-step menu-percent\" role=\"menuitem\" title=\"Reset the text size\" aria-label=\"Reset the text size\" @click=\"setTextScale(1)\">{{ Math.round(textScale * 100) }}%</button> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Bigger text\" aria-label=\"Bigger text\" :disabled=\"!canGrowText\" @click=\"changeTextScale(1)\">A+</button></div><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"openFile()\"><span class=\"menu-icon\"><icon name=\"open\"></icon></span>Open&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"saveFile()\"><span class=\"menu-icon\"><icon name=\"save\"></icon></span>Save&hellip;</button><div class=\"menu-separator\"></div><button v-if=\"isPopOut\" class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); bringBack()\"><span class=\"menu-icon\"><icon name=\"popIn\"></icon></span>Bring back into the page</button> <button v-else class=\"menu-item\" role=\"menuitem\" @click=\"popOut()\"><span class=\"menu-icon\"><icon name=\"popOut\"></icon></span>Pop out into a window</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('html')\"><span class=\"menu-icon\"><icon name=\"code\"></icon></span>Copy as HTML&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); resetConsole()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Reset&hellip;</button><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('about')\"><span class=\"menu-icon\"><icon name=\"info\"></icon></span>About JS Box</button></div><div v-if=\"isHistoryOpen\" class=\"more-menu history-panel\" role=\"menu\" aria-label=\"History\" ref=\"historyPanel\" @keydown=\"onMenuKeyDown\"><div class=\"history-title\">History</div><div v-if=\"!historyEntries.length\" class=\"history-empty\">Code that you run will show up here.</div><button v-for=\"entry in historyEntries\" class=\"menu-item history-item\" role=\"menuitem\" :title=\"commandHistory[entry.index]\" @click=\"recallHistory(entry.index)\"><span class=\"history-label\">{{ entry.label }}</span> <span class=\"history-detail\">{{ entry.detail }}</span></button><div class=\"history-hint\">Tip: press &uarr; at the start of the editor (or &darr; at the end) to go through the history.</div></div><div v-if=\"valueMenu\" class=\"more-menu value-menu\" role=\"menu\" aria-label=\"Value\" ref=\"valueMenu\" :style=\"{left: valueMenu.x + 'px', top: valueMenu.y + 'px'}\" @keydown=\"onMenuKeyDown\"><button class=\"menu-item\" role=\"menuitem\" @click=\"copyValueAsJson()\"><span class=\"menu-icon\"><icon name=\"copy\"></icon></span>Copy as JSON</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"saveValueAsJson()\"><span class=\"menu-icon\"><icon name=\"save\"></icon></span>Save as JSON&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"storeValueAsGlobal()\"><span class=\"menu-icon\"><icon name=\"variable\"></icon></span>Store as global variable</button> <button v-if=\"valueMenu.propertyPath\" class=\"menu-item\" role=\"menuitem\" @click=\"copyPropertyPath()\"><span class=\"menu-icon\"><icon name=\"path\"></icon></span>Copy property path</button><template v-if=\"!valueMenu.isPrimitive\"><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"refreshValue()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Refresh</button></template></div><div v-if=\"toast\" class=\"toast\" role=\"status\">{{ toast.message }}</div><div v-if=\"isPoppedOut\" class=\"popped-out\"><div class=\"popped-out-content\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"popped-out-title\">This console is open in a separate window</div><div class=\"popped-out-text\">Its code still runs in this page.</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"focusPopOut\">Show the window</button> <button class=\"dialog-button primary\" @click=\"requestPopIn\">Bring it back</button></div></div></div><div v-if=\"isAboutOpen\" class=\"dialog-backdrop\" @mousedown.self=\"closeAbout\" @keydown.esc=\"closeAbout\"><div :class=\"['dialog', 'about-dialog', aboutTab === 'html' ? 'is-export' : '']\" role=\"dialog\" aria-modal=\"true\" aria-label=\"About JS Box\"><div class=\"about-header\"><div class=\"tabs\" role=\"tablist\"><button role=\"tab\" :aria-selected=\"aboutTab === 'about'\" :class=\"['tab', aboutTab === 'about' ? 'active' : '']\" @click=\"aboutTab = 'about'\">About</button> <button role=\"tab\" :aria-selected=\"aboutTab === 'html'\" :class=\"['tab', aboutTab === 'html' ? 'active' : '']\" @click=\"aboutTab = 'html'\">Copy as HTML</button></div><button class=\"close-button\" ref=\"aboutCloseButton\" title=\"Close\" @click=\"closeAbout\"><icon name=\"close\"></icon></button></div><div v-if=\"aboutTab === 'about'\" class=\"about-body\"><div class=\"about-title\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div><div class=\"about-name\">YourJS Box <span class=\"about-aka\">aka JS Box</span></div><div class=\"about-version\">Version {{ packageInfo.version }}</div></div></div><p class=\"about-description\">An interactive JavaScript console that can be embedded in any web page with a single script tag.</p><div class=\"about-links\"><a :href=\"packageInfo.homepage\" target=\"_blank\" rel=\"noopener\">Website</a> <a :href=\"packageInfo.repoUrl\" target=\"_blank\" rel=\"noopener\">GitHub</a> <a :href=\"packageInfo.repoUrl + '#readme'\" target=\"_blank\" rel=\"noopener\">Documentation</a> <a :href=\"packageInfo.bugsUrl\" target=\"_blank\" rel=\"noopener\">Report an issue</a></div><h3>This console</h3><dl class=\"about-details\"><dt>Code runs in</dt><dd>{{ runnerDescription }}</dd><dt>Theme</dt><dd>{{ themeDescription }}</dd><dt>Code blocks run as</dt><dd>{{ blockTypeDescription }}</dd><dt>Imports</dt><dd>{{ importsUrl ? 'Packages imported by name load from ' + importsUrl.replace('{specifier}', '\u2026') : 'Only URLs and paths can be imported' }}</dd><dt>Results</dt><dd>{{ showResults ? 'The value of the last expression is shown' : 'Not shown' }}</dd><dt>Layout</dt><dd>{{ layoutDescription }}</dd></dl><h3>Keyboard shortcuts</h3><dl class=\"about-details\"><dt><kbd>{{ modKey }}</kbd> + <kbd>Enter</kbd></dt><dd>Run the next block of code</dd><dt><kbd>&uarr;</kbd> / <kbd>&darr;</kbd></dt><dd>Go through the code that was run (at the very start or end of the editor)</dd><dt><kbd>{{ modKey === 'Ctrl' ? 'Alt' : '&#8997;' }}</kbd> + <kbd>H</kbd></dt><dd>Show the history</dd><dt><kbd>Esc</kbd></dt><dd>Close this window, the menu or full screen</dd></dl><div class=\"about-footer\">MIT License &copy; 2023-present Chris West &middot; Built with Vue {{ libraryVersions.vue }}, Ace {{ libraryVersions['ace-builds'] }}, Prism {{ libraryVersions.prismjs }} and Acorn {{ libraryVersions.acorn }}</div></div><div v-else class=\"about-body export-body\"><div class=\"export-options\"><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Code\"><button v-for=\"option in exportCodeOptions\" role=\"radio\" :aria-checked=\"exportCode === option.value\" :class=\"exportCode === option.value ? 'active' : ''\" @click=\"exportCode = option.value\">{{ option.label }}</button></div><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Format\"><button v-for=\"option in exportFormatOptions\" role=\"radio\" :aria-checked=\"exportFormat === option.value\" :class=\"exportFormat === option.value ? 'active' : ''\" @click=\"exportFormat = option.value\">{{ option.label }}</button></div></div><div class=\"export-note\">{{ exportNote }}</div><div class=\"export-preview\"><ace-editor :model-value=\"exportHtml\" language=\"html\" :theme=\"theme\" height=\"100%\" :read-only=\"true\"></ace-editor></div><div class=\"export-actions\"><button class=\"dialog-button\" @click=\"downloadExport\">Download page</button> <button class=\"dialog-button primary\" @click=\"copyExport\">{{ copyLabel }}</button></div></div></div></div><div v-if=\"dialog\" class=\"dialog-backdrop\" @mousedown.self=\"closeDialog(false)\" @keydown.esc=\"closeDialog(false)\"><div class=\"dialog\" role=\"alertdialog\" aria-modal=\"true\" :aria-label=\"dialog.title\"><div class=\"dialog-title\">{{ dialog.title }}</div><div class=\"dialog-message\">{{ dialog.message }}</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"closeDialog(false)\">Cancel</button> <button class=\"dialog-button primary\" ref=\"dialogConfirmButton\" @click=\"closeDialog(true)\">{{ dialog.confirmText }}</button></div></div></div></div>";
17
+ const VIEWER_IFRAME_HTML = "<div id=\"splash\" aria-label=\"Loading\"><div class=\"splash-content\"><span class=\"logo logo-large\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"splash-progress\"><div></div></div><div class=\"splash-message splash-slow\">Still loading&hellip;</div><div class=\"splash-message splash-error\">JS Box couldn&rsquo;t load. Please check your connection and reload the page.</div></div></div><div id=\"vueApp\" v-cloak><input type=\"file\" ref=\"fileInput\" class=\"file-input\" accept=\".js,.mjs,.cjs,.ts,.mts,.cts,.txt,text/javascript,text/plain\" tabindex=\"-1\" aria-hidden=\"true\" @change=\"onFileChosen\"><div id=\"main\" ref=\"main\" :class=\"mainElemClassNames\" :style=\"mainElemStyles\" @mousedown=\"onMainElemMouseDown\"><div id=\"displays\"><div ref=\"displaysScroller\" @scroll=\"onDisplaysScroll\"><template v-for=\"display in displays\"><div v-if=\"display.type === 'prism'\"><div v-if=\"display.isHidden\" class=\"code-header toggleable no-select\" @click=\"display.isCodeShown = !display.isCodeShown\" :title=\"display.isCodeShown ? 'Hide Code' : 'Show Code'\"><span :class=\"['arrow', 'expandable', display.isCodeShown ? 'expanded' : '']\"></span>{{ display.header || 'Hidden code' }}</div><div v-else-if=\"display.header\" class=\"code-header\">{{ display.header }}</div><div v-if=\"display.isCodeShown\" class=\"console-row code-row\"><span class=\"row-icon\"><icon name=\"chevron\"></icon></span><prism :language=\"language\" :code=\"display.value\" :is-dark=\"theme === 'dark'\" match-braces></prism><button class=\"copy-to-editor-button\" title=\"Copy to editor\" @click=\"copyToEditor(display.value)\"><icon name=\"copyToEditor\"></icon></button></div></div><div v-if=\"display.type === 'log' &amp;&amp; !isInCollapsedGroup(display)\" :class=\"['console-row', 'log-' + display.key, display.groupId ? 'group-header' : '']\" :style=\"{'--depth': display.groupIds.length}\" :title=\"display.name\" @click=\"onLogRowClick(display, $event)\"><span v-if=\"display.key === 'error' || display.key === 'assert'\" class=\"row-icon\"><icon name=\"consoleError\"></icon></span><span v-else-if=\"display.key === 'warn'\" class=\"row-icon\"><icon name=\"consoleWarning\"></icon></span><span v-else-if=\"display.key === 'result'\" class=\"row-icon\"><icon name=\"result\"></icon></span><span v-else-if=\"display.groupId\" :class=\"['row-icon', 'arrow', 'expandable', display.isCollapsed ? '' : 'expanded']\"></span><div class=\"row-content\"><table v-if=\"display.table\" class=\"console-table\"><thead><tr><th v-for=\"header in display.table.headers\">{{ header }}</th></tr></thead><tbody><tr v-for=\"row in display.table.rows\"><td>{{ row.index }}</td><td v-for=\"cell in row.cells\"><template v-if=\"cell\"><span v-for=\"part in cell.parts\" :class=\"'t-' + part[0]\">{{ part[1] }}</span></template></td></tr></tbody></table><js-value v-for=\"(description, index) in display.descriptions\" :description=\"description\" :key-path=\"[]\" :path=\"[display.logId, index]\"></js-value><div v-if=\"display.stack\" class=\"trace-stack\">{{ display.stack }}</div></div></div><div v-if=\"display.type === 'notice'\" class=\"console-row notice\">{{ display.message }}</div><div v-if=\"display.type === 'error'\" class=\"console-row log-error\"><span class=\"row-icon\"><icon name=\"consoleError\"></icon></span><div class=\"row-content\">{{ display.message }}</div></div></template></div></div><div class=\"divider\" ref=\"mainDivider\"></div><div id=\"editor\"><ace-editor ref=\"editor\" v-model=\"jsCode\" :language=\"language\" :theme=\"theme\" :font-size=\"Math.round(12 * textScale)\" @key-combo=\"onEditorKeyCombo\" @history-key=\"onEditorHistoryKey\" height=\"100%\"></ace-editor></div></div><div id=\"bottomNav\"><div class=\"brand\"><button class=\"logo-button\" title=\"About JS Box\" @click=\"openAbout('about')\"><span class=\"logo\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span></button> <span class=\"runner-badge\" :title=\"runnerMode === 'window' ? 'Code runs directly in this page' : 'Code runs in a Web Worker (no DOM access)'\">{{ runnerMode === 'window' ? 'Window' : 'Worker' }}</span></div><div class=\"buttons\"><template v-for=\"bottomButton in bottomButtons\"><span v-if=\"bottomButton.isSeparator\" class=\"separator\"></span> <button v-else :class=\"bottomButton.className\" @click=\"bottomButton.callback.call(this, $event)\" :title=\"bottomButton.title\" :disabled=\"bottomButton.disableIf &amp;&amp; bottomButton.disableIf.call(this)\"><icon :name=\"bottomButton.iconName\"></icon><span v-if=\"bottomButton.label\" class=\"label\">{{ bottomButton.label }}</span></button></template></div></div><div v-if=\"isMenuOpen\" class=\"more-menu\" role=\"menu\" aria-label=\"More\" ref=\"moreMenu\" @keydown=\"onMenuKeyDown\"><div class=\"menu-row\" role=\"group\" aria-label=\"Text size\"><span class=\"menu-row-label\">Text size</span> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Smaller text\" aria-label=\"Smaller text\" :disabled=\"!canShrinkText\" @click=\"changeTextScale(-1)\">A&minus;</button> <button class=\"menu-item menu-step menu-percent\" role=\"menuitem\" title=\"Reset the text size\" aria-label=\"Reset the text size\" @click=\"setTextScale(1)\">{{ Math.round(textScale * 100) }}%</button> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Bigger text\" aria-label=\"Bigger text\" :disabled=\"!canGrowText\" @click=\"changeTextScale(1)\">A+</button></div><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"openFile()\"><span class=\"menu-icon\"><icon name=\"open\"></icon></span>Open&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"saveFile()\"><span class=\"menu-icon\"><icon name=\"save\"></icon></span>Save&hellip;</button><div class=\"menu-separator\"></div><button v-if=\"isPopOut\" class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); bringBack()\"><span class=\"menu-icon\"><icon name=\"popIn\"></icon></span>Bring back into the page</button> <button v-else class=\"menu-item\" role=\"menuitem\" @click=\"popOut()\"><span class=\"menu-icon\"><icon name=\"popOut\"></icon></span>Pop out into a window</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('html')\"><span class=\"menu-icon\"><icon name=\"code\"></icon></span>Copy as HTML&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); resetConsole()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Reset&hellip;</button><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('about')\"><span class=\"menu-icon\"><icon name=\"info\"></icon></span>About JS Box</button></div><div v-if=\"isHistoryOpen\" class=\"more-menu history-panel\" role=\"menu\" aria-label=\"History\" ref=\"historyPanel\" @keydown=\"onMenuKeyDown\"><div class=\"history-title\">History</div><div v-if=\"!historyEntries.length\" class=\"history-empty\">Code that you run will show up here.</div><button v-for=\"entry in historyEntries\" class=\"menu-item history-item\" role=\"menuitem\" :title=\"commandHistory[entry.index]\" @click=\"recallHistory(entry.index)\"><span class=\"history-label\">{{ entry.label }}</span> <span class=\"history-detail\">{{ entry.detail }}</span></button><div class=\"history-hint\">Tip: press &uarr; at the start of the editor (or &darr; at the end) to go through the history.</div></div><div v-if=\"valueMenu\" class=\"more-menu value-menu\" role=\"menu\" aria-label=\"Value\" ref=\"valueMenu\" :style=\"{left: valueMenu.x + 'px', top: valueMenu.y + 'px'}\" @keydown=\"onMenuKeyDown\"><button class=\"menu-item\" role=\"menuitem\" @click=\"copyValueAsJson()\"><span class=\"menu-icon\"><icon name=\"copy\"></icon></span>Copy as JSON</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"saveValueAsJson()\"><span class=\"menu-icon\"><icon name=\"save\"></icon></span>Save as JSON&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"storeValueAsGlobal()\"><span class=\"menu-icon\"><icon name=\"variable\"></icon></span>Store as global variable</button> <button v-if=\"valueMenu.propertyPath\" class=\"menu-item\" role=\"menuitem\" @click=\"copyPropertyPath()\"><span class=\"menu-icon\"><icon name=\"path\"></icon></span>Copy property path</button><template v-if=\"!valueMenu.isPrimitive\"><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"refreshValue()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Refresh</button></template></div><div v-if=\"toast\" class=\"toast\" role=\"status\">{{ toast.message }}</div><div v-if=\"isPoppedOut\" class=\"popped-out\"><div class=\"popped-out-content\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"popped-out-title\">This console is open in a separate window</div><div class=\"popped-out-text\">Its code still runs in this page.</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"focusPopOut\">Show the window</button> <button class=\"dialog-button primary\" @click=\"requestPopIn\">Bring it back</button></div></div></div><div v-if=\"isAboutOpen\" class=\"dialog-backdrop\" @mousedown.self=\"closeAbout\" @keydown.esc=\"closeAbout\"><div :class=\"['dialog', 'about-dialog', aboutTab === 'html' ? 'is-export' : '']\" role=\"dialog\" aria-modal=\"true\" aria-label=\"About JS Box\"><div class=\"about-header\"><div class=\"tabs\" role=\"tablist\"><button role=\"tab\" :aria-selected=\"aboutTab === 'about'\" :class=\"['tab', aboutTab === 'about' ? 'active' : '']\" @click=\"aboutTab = 'about'\">About</button> <button role=\"tab\" :aria-selected=\"aboutTab === 'html'\" :class=\"['tab', aboutTab === 'html' ? 'active' : '']\" @click=\"aboutTab = 'html'\">Copy as HTML</button></div><button class=\"close-button\" ref=\"aboutCloseButton\" title=\"Close\" @click=\"closeAbout\"><icon name=\"close\"></icon></button></div><div v-if=\"aboutTab === 'about'\" class=\"about-body\"><div class=\"about-title\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div><div class=\"about-name\">YourJS Box <span class=\"about-aka\">aka JS Box</span></div><div class=\"about-version\">Version {{ packageInfo.version }}</div></div></div><p class=\"about-description\">An interactive JavaScript console that can be embedded in any web page with a single script tag.</p><div class=\"about-links\"><a :href=\"packageInfo.homepage\" target=\"_blank\" rel=\"noopener\">Website</a> <a :href=\"packageInfo.repoUrl\" target=\"_blank\" rel=\"noopener\">GitHub</a> <a :href=\"packageInfo.repoUrl + '#readme'\" target=\"_blank\" rel=\"noopener\">Documentation</a> <a :href=\"packageInfo.bugsUrl\" target=\"_blank\" rel=\"noopener\">Report an issue</a></div><h3>This console</h3><dl class=\"about-details\"><dt>Code runs in</dt><dd>{{ runnerDescription }}</dd><dt>Theme</dt><dd>{{ themeDescription }}</dd><dt>Language</dt><dd>{{ isTypeScript ? 'TypeScript (types are removed by Babel, not checked)' : 'JavaScript' }}</dd><dt>Code blocks run as</dt><dd>{{ blockTypeDescription }}</dd><dt>Imports</dt><dd>{{ importsUrl ? 'Packages imported by name load from ' + importsUrl.replace('{specifier}', '\u2026') : 'Only URLs and paths can be imported' }}</dd><dt>Results</dt><dd>{{ showResults ? 'The value of the last expression is shown' : 'Not shown' }}</dd><dt>Layout</dt><dd>{{ layoutDescription }}</dd></dl><h3>Keyboard shortcuts</h3><dl class=\"about-details\"><dt><kbd>{{ modKey }}</kbd> + <kbd>Enter</kbd></dt><dd>Run the next block of code</dd><dt><kbd>&uarr;</kbd> / <kbd>&darr;</kbd></dt><dd>Go through the code that was run (at the very start or end of the editor)</dd><dt><kbd>{{ modKey === 'Ctrl' ? 'Alt' : '&#8997;' }}</kbd> + <kbd>H</kbd></dt><dd>Show the history</dd><dt><kbd>Esc</kbd></dt><dd>Close this window, the menu or full screen</dd></dl><div class=\"about-footer\">MIT License &copy; 2023-present Chris West &middot; Built with {{ builtWith }}</div></div><div v-else class=\"about-body export-body\"><div class=\"export-options\"><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Code\"><button v-for=\"option in exportCodeOptions\" role=\"radio\" :aria-checked=\"exportCode === option.value\" :class=\"exportCode === option.value ? 'active' : ''\" @click=\"exportCode = option.value\">{{ option.label }}</button></div><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Format\"><button v-for=\"option in exportFormatOptions\" role=\"radio\" :aria-checked=\"exportFormat === option.value\" :class=\"exportFormat === option.value ? 'active' : ''\" @click=\"exportFormat = option.value\">{{ option.label }}</button></div></div><div class=\"export-note\">{{ exportNote }}</div><div class=\"export-preview\"><ace-editor :model-value=\"exportHtml\" language=\"html\" :theme=\"theme\" height=\"100%\" :read-only=\"true\"></ace-editor></div><div class=\"export-actions\"><button class=\"dialog-button\" @click=\"downloadExport\">Download page</button> <button class=\"dialog-button primary\" @click=\"copyExport\">{{ copyLabel }}</button></div></div></div></div><div v-if=\"dialog\" class=\"dialog-backdrop\" @mousedown.self=\"closeDialog(false)\" @keydown.esc=\"closeDialog(false)\"><div class=\"dialog\" role=\"alertdialog\" aria-modal=\"true\" :aria-label=\"dialog.title\"><div class=\"dialog-title\">{{ dialog.title }}</div><div class=\"dialog-message\">{{ dialog.message }}</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"closeDialog(false)\">Cancel</button> <button class=\"dialog-button primary\" ref=\"dialogConfirmButton\" @click=\"closeDialog(true)\">{{ dialog.confirmText }}</button></div></div></div></div>";
18
18
  /**
19
19
  * The URL of this script which is used to remove this script's lines from the
20
20
  * stack traces of errors in window mode.
@@ -33,6 +33,8 @@
33
33
  * was tested with.
34
34
  */
35
35
  const LIBRARY_VERSIONS = {
36
+ // Only loaded by TypeScript consoles.
37
+ '@babel/standalone': '7.29.9',
36
38
  'ace-builds': '1.44.0',
37
39
  'acorn': '8.18.0',
38
40
  'prism-themes': '1.9.0',
@@ -138,6 +140,9 @@
138
140
  // How wrapping the last expression of each block shifted the columns on its
139
141
  // line so that stack traces can show the original columns.
140
142
  const columnShiftsBySnippet = {};
143
+ // For TypeScript, where each position in the code that ran came from in the
144
+ // TypeScript code (see runCode()).
145
+ const sourceMapsBySnippet = {};
141
146
 
142
147
  const counts = {};
143
148
  const timers = {};
@@ -334,15 +339,40 @@
334
339
  .split('\n')
335
340
  .filter(line => !(ownUrl && /^\s*at\b/.test(line) && line.includes(ownUrl)))
336
341
  .join('\n')
337
- // Undoes the shift in columns caused by wrapping the last expression.
338
- .replace(/(snippet-\d+\.js):(\d+):(\d+)/g, (match, name, line, column) => {
342
+ .replace(/(snippet-\d+\.[jt]s):(\d+):(\d+)/g, (match, name, line, column) => {
343
+ line = +line;
344
+ column = +column;
345
+ // Undoes the shift in columns caused by wrapping the last expression.
339
346
  const info = columnShiftsBySnippet[name];
340
- return info && +line === info.line && +column > info.column
341
- ? `${name}:${line}:${Math.max(info.column, column - info.shift)}`
342
- : match;
347
+ if (info && line === info.line && column > info.column) {
348
+ column = Math.max(info.column, column - info.shift);
349
+ }
350
+ [line, column] = toSourcePosition(sourceMapsBySnippet[name], line, column);
351
+ return `${name}:${line}:${column}`;
343
352
  });
344
353
  }
345
354
 
355
+ /**
356
+ * Finds where a position in compiled code came from in the original code.
357
+ * @param {number[][][]=} sourceMap
358
+ * For each line of the compiled code, its mappings as [compiled column,
359
+ * original line, original column] (all starting at 0) sorted by column.
360
+ * @param {number} line
361
+ * @param {number} column
362
+ * @returns {[number, number]}
363
+ * The original line and column (both starting at 1) or the given ones if
364
+ * the position isn't mapped.
365
+ */
366
+ function toSourcePosition(sourceMap, line, column) {
367
+ const mappings = sourceMap?.[line - 1];
368
+ let best;
369
+ for (const mapping of mappings ?? []) {
370
+ if (mapping[0] > column - 1) break;
371
+ best = mapping;
372
+ }
373
+ return best ? [best[1] + 1, best[2] + 1 + (column - 1 - best[0])] : [line, column];
374
+ }
375
+
346
376
  function without(props, obj) {
347
377
  if (!obj) return obj => without(props, obj);
348
378
  const ret = {...obj};
@@ -709,12 +739,22 @@
709
739
  * @param {[number, number]=} runOptions.resultRange
710
740
  * The start and end index of the last expression in the code whose value
711
741
  * should be shown.
742
+ * @param {"javascript"|"typescript"=} runOptions.language
743
+ * The language that the code was written in. TypeScript is compiled to
744
+ * JavaScript before it is sent here.
745
+ * @param {number[][][]=} runOptions.sourceMap
746
+ * For TypeScript, where the compiled code came from (see
747
+ * toSourcePosition()).
748
+ * @param {{message: string, line: number, column: number}=} runOptions.compileError
749
+ * For TypeScript, the syntax error that kept the code from being compiled
750
+ * which is reported instead of running the code.
712
751
  */
713
752
  async function runCode(jsCode, runOptions) {
714
- const {blockType, resultRange} = Object(runOptions);
753
+ const {blockType, resultRange, language, sourceMap, compileError} = Object(runOptions);
715
754
 
716
755
  // Names the code so that stack traces refer to it by this name.
717
- const snippetName = `snippet-${++snippetCount}.js`;
756
+ const snippetName = `snippet-${++snippetCount}.${language === 'typescript' ? 'ts' : 'js'}`;
757
+ if (Array.isArray(sourceMap)) sourceMapsBySnippet[snippetName] = sourceMap;
718
758
 
719
759
  // Wraps the last expression so that its value is reported. This keeps
720
760
  // everything on the same lines so that line numbers in errors still match
@@ -735,7 +775,17 @@
735
775
 
736
776
  runningBlockType = blockType === 'module' ? 'module' : 'classic';
737
777
  try {
738
- if (blockType === 'module') {
778
+ if (compileError) {
779
+ // Reported here so that it shows up after anything logged by the code
780
+ // that ran before it.
781
+ const {message, line, column} = compileError;
782
+ reportUncaught({
783
+ name: 'SyntaxError',
784
+ message: `${message}`,
785
+ stack: `SyntaxError: ${message}\n at ${snippetName}:${+line}:${+column}`,
786
+ });
787
+ }
788
+ else if (blockType === 'module') {
739
789
  // Waits for the module to finish (including any top-level await).
740
790
  await import('data:text/javascript;charset=utf-8,' + encodeURIComponent(source));
741
791
  }
@@ -1045,6 +1095,7 @@
1045
1095
  function createConsole({code, dataset, height, insert}) {
1046
1096
  const runnerMode = dataset.runner === 'window' ? 'window' : 'worker';
1047
1097
  const blockType = dataset.blockType === 'module' ? 'module' : 'classic';
1098
+ const language = dataset.language === 'typescript' ? 'typescript' : 'javascript';
1048
1099
  const showResults = dataset.showResults !== 'false';
1049
1100
  // An empty data-imports-url turns off loading packages by name.
1050
1101
  const importsUrl = dataset.importsUrl == null
@@ -1236,6 +1287,12 @@
1236
1287
  */
1237
1288
  function init(jsCode, dataset, meta) {
1238
1289
  if (IS_MISSING_LIBRARIES) return;
1290
+ // TypeScript can't run without Babel.
1291
+ if (meta.language === 'typescript' && !window.Babel) {
1292
+ document.querySelector('#splash').classList.add('failed');
1293
+ return;
1294
+ }
1295
+ Prism.plugins.autoloader.loadLanguages(meta.language);
1239
1296
 
1240
1297
  const hidePrefix = dataset.hidePrefix ?? '';
1241
1298
  const darkSchemeQuery = matchMedia('(prefers-color-scheme: dark)');
@@ -1259,6 +1316,7 @@
1259
1316
  // The name of the file that was last opened or saved.
1260
1317
  fileName: null,
1261
1318
  blockType: meta.blockType,
1319
+ language: meta.language,
1262
1320
  showResults: meta.showResults,
1263
1321
  packageInfo: meta.packageInfo,
1264
1322
  libraryVersions: meta.libraryVersions,
@@ -1334,6 +1392,21 @@
1334
1392
  const name = this.theme === 'dark' ? 'Dark' : 'Light';
1335
1393
  return `${name} (${this.forcedTheme ? 'set by data-theme' : 'follows your system'})`;
1336
1394
  },
1395
+ isTypeScript() {
1396
+ return this.language === 'typescript';
1397
+ },
1398
+ /** The libraries (and their versions) that this console uses. */
1399
+ builtWith() {
1400
+ const versions = this.libraryVersions;
1401
+ const names = [
1402
+ `Vue ${versions.vue}`,
1403
+ `Ace ${versions['ace-builds']}`,
1404
+ `Prism ${versions.prismjs}`,
1405
+ `Acorn ${versions.acorn}`,
1406
+ ...this.isTypeScript ? [`Babel ${versions['@babel/standalone']}`] : [],
1407
+ ];
1408
+ return names.slice(0, -1).join(', ') + ' and ' + names[names.length - 1];
1409
+ },
1337
1410
  blockTypeDescription() {
1338
1411
  return this.blockType === 'module'
1339
1412
  ? 'Modules (top-level await and imports work, declarations stay in each block)'
@@ -1663,17 +1736,29 @@
1663
1736
  this.commandHistory.push(historyCode);
1664
1737
  if (this.commandHistory.length > 200) this.commandHistory.shift();
1665
1738
  }
1666
- const {code, resultRange} = prepareCode(group.lines, {
1667
- blockType: this.blockType,
1668
- importsUrl: this.importsUrl,
1669
- // Like the browser's console, show the value of the last
1670
- // expression (except for hidden code).
1671
- findResult: this.showResults && !isHidden,
1672
- });
1739
+ const {language, blockType} = this;
1740
+ const compiled = language === 'typescript'
1741
+ ? compileTypeScript(group.lines, {blockType})
1742
+ : {code: group.lines};
1743
+ const {code, resultRange} = compiled.error
1744
+ ? {code: '', resultRange: null}
1745
+ : prepareCode(compiled.code, {
1746
+ blockType,
1747
+ importsUrl: this.importsUrl,
1748
+ // Like the browser's console, show the value of the last
1749
+ // expression (except for hidden code).
1750
+ findResult: this.showResults && !isHidden,
1751
+ });
1673
1752
  messageParent({
1674
1753
  target: 'runner',
1675
1754
  func: 'runCode',
1676
- args: [code, {blockType: this.blockType, resultRange}],
1755
+ args: [code, {
1756
+ blockType,
1757
+ resultRange,
1758
+ language,
1759
+ sourceMap: compiled.sourceMap,
1760
+ compileError: compiled.error,
1761
+ }],
1677
1762
  });
1678
1763
  },
1679
1764
  clearConsole() {
@@ -1747,14 +1832,17 @@
1747
1832
  async saveFile() {
1748
1833
  this.closeMenu();
1749
1834
  const code = this.currentCode + '\n';
1750
- const suggestedName = this.fileName || 'js-box.js';
1835
+ const suggestedName = this.fileName || (this.isTypeScript ? 'js-box.ts' : 'js-box.js');
1836
+ const fileType = this.isTypeScript
1837
+ ? {description: 'TypeScript', accept: {'text/typescript': ['.ts', '.mts']}}
1838
+ : {description: 'JavaScript', accept: {'text/javascript': ['.js', '.mjs']}};
1751
1839
 
1752
1840
  // Shows a "Save as" dialog where it is supported.
1753
1841
  if ('function' === typeof window.showSaveFilePicker) {
1754
1842
  try {
1755
1843
  const handle = await showSaveFilePicker({
1756
1844
  suggestedName,
1757
- types: [{description: 'JavaScript', accept: {'text/javascript': ['.js', '.mjs']}}],
1845
+ types: [fileType],
1758
1846
  });
1759
1847
  const writable = await handle.createWritable();
1760
1848
  await writable.write(code);
@@ -1768,7 +1856,7 @@
1768
1856
  }
1769
1857
  }
1770
1858
 
1771
- const url = URL.createObjectURL(new Blob([code], {type: 'text/javascript'}));
1859
+ const url = URL.createObjectURL(new Blob([code], {type: Object.keys(fileType.accept)[0]}));
1772
1860
  Object.assign(document.createElement('a'), {href: url, download: suggestedName}).click();
1773
1861
  setTimeout(() => URL.revokeObjectURL(url), 1000);
1774
1862
  },
@@ -2510,7 +2598,7 @@
2510
2598
 
2511
2599
  codeElem.textContent = this.code;
2512
2600
 
2513
- codeElem.className = `language-javascript`;
2601
+ codeElem.className = `language-${this.language ?? 'javascript'}`;
2514
2602
  preElem.className = '';
2515
2603
 
2516
2604
  for (const propName of ['lineNumbers', 'matchBraces']) {
@@ -2783,6 +2871,88 @@
2783
2871
  };
2784
2872
  }
2785
2873
 
2874
+ /**
2875
+ * Compiles TypeScript to JavaScript by removing the types (without checking
2876
+ * them) using Babel. Every line stays on the same line so that line numbers
2877
+ * in errors still match.
2878
+ * @param {string} code
2879
+ * @param {Object} options
2880
+ * @param {"classic"|"module"} options.blockType
2881
+ * @returns {{code: string, sourceMap?: number[][][], error?: {message: string, line: number, column: number}}}
2882
+ * The JavaScript code and, for each of its lines, where it came from (see
2883
+ * decodeSourceMap()) or the syntax error that kept it from being compiled.
2884
+ */
2885
+ function compileTypeScript(code, {blockType}) {
2886
+ try {
2887
+ const result = Babel.transform(code, {
2888
+ filename: 'snippet.ts',
2889
+ presets: [['typescript', {
2890
+ allExtensions: true,
2891
+ // Only `import type` is removed so that an import is never dropped
2892
+ // just because it isn't used yet.
2893
+ onlyRemoveTypeImports: true,
2894
+ }]],
2895
+ sourceType: blockType === 'module' ? 'module' : 'script',
2896
+ // Allows `export` in a namespace in classic blocks. Imports in classic
2897
+ // blocks are left in so that running them shows the usual error.
2898
+ parserOpts: {allowImportExportEverywhere: blockType !== 'module'},
2899
+ retainLines: true,
2900
+ sourceMaps: true,
2901
+ });
2902
+ return {code: result.code, sourceMap: decodeSourceMap(result.map.mappings)};
2903
+ }
2904
+ catch (e) {
2905
+ if (e?.name !== 'SyntaxError' || !e.loc) throw e;
2906
+ return {
2907
+ code,
2908
+ error: {
2909
+ // Babel's message starts with the file name and ends with the
2910
+ // position and the code where the error is.
2911
+ message: `${e.message}`.split('\n')[0].replace(/^\/?snippet\.ts:\s*/, '').replace(/\s*\(\d+:\d+\)$/, ''),
2912
+ line: e.loc.line,
2913
+ column: e.loc.column + 1,
2914
+ },
2915
+ };
2916
+ }
2917
+ }
2918
+
2919
+ /**
2920
+ * Decodes the mappings of a source map (for a single source).
2921
+ * @param {string} mappings
2922
+ * @returns {number[][][]}
2923
+ * For each line of the compiled code, its mappings as [compiled column,
2924
+ * original line, original column] (all starting at 0) sorted by column.
2925
+ */
2926
+ function decodeSourceMap(mappings) {
2927
+ const BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
2928
+ const totals = [0, 0, 0, 0, 0];
2929
+ return mappings.split(';').map(line => {
2930
+ totals[0] = 0;
2931
+ const lineMappings = [];
2932
+ for (const segment of line.split(',')) {
2933
+ if (!segment) continue;
2934
+ const fields = [];
2935
+ let value = 0, shift = 0;
2936
+ for (const char of segment) {
2937
+ const digit = BASE64.indexOf(char);
2938
+ value += (digit & 31) << shift;
2939
+ if (digit & 32) {
2940
+ shift += 5;
2941
+ }
2942
+ else {
2943
+ fields.push(value & 1 ? -(value >>> 1) : value >>> 1);
2944
+ value = shift = 0;
2945
+ }
2946
+ }
2947
+ // The fields are the compiled column, the source index, the original
2948
+ // line and the original column, each relative to the previous one.
2949
+ fields.forEach((field, index) => totals[index] += field);
2950
+ if (fields.length >= 4) lineMappings.push([totals[0], totals[2], totals[3]]);
2951
+ }
2952
+ return lineMappings.sort((a, b) => a[0] - b[0]);
2953
+ });
2954
+ }
2955
+
2786
2956
  /**
2787
2957
  * Gets a block of code ready to run: packages imported by name (eg.
2788
2958
  * `import _ from 'lodash'` or `import('lodash')`) are loaded from importsUrl
@@ -3223,6 +3393,8 @@
3223
3393
  // Used to find the last expression in each block of code (so that its
3224
3394
  // value can be shown) and the packages that it imports.
3225
3395
  libraryUrl('acorn', 'dist/acorn.js'),
3396
+ // Removes the types from TypeScript code before it is run.
3397
+ ...language === 'typescript' ? [libraryUrl('@babel/standalone', 'babel.min.js')] : [],
3226
3398
  ],
3227
3399
  cssUrls: [
3228
3400
  'data:text/css,' + encodeURIComponent(VIEWER_IFRAME_CSS),
@@ -3241,6 +3413,7 @@
3241
3413
  this.call('init', code, dataset, {
3242
3414
  runnerMode,
3243
3415
  blockType,
3416
+ language,
3244
3417
  showResults,
3245
3418
  importsUrl,
3246
3419
  packageInfo: PACKAGE_INFO,
@@ -3317,7 +3490,7 @@
3317
3490
  * The options of YourJSBox.create() which are the same as the data
3318
3491
  * attributes of a script tag.
3319
3492
  */
3320
- const CONSOLE_OPTION_NAMES = ['blockType', 'dividerOrient', 'hidePrefix', 'importsUrl', 'librariesUrl', 'runner', 'showResults', 'theme'];
3493
+ const CONSOLE_OPTION_NAMES = ['blockType', 'dividerOrient', 'hidePrefix', 'importsUrl', 'language', 'librariesUrl', 'runner', 'showResults', 'theme'];
3321
3494
 
3322
3495
  /**
3323
3496
  * The JavaScript API for creating consoles (available as window.YourJSBox).
@@ -3343,6 +3516,7 @@
3343
3516
  * Optional. The code that the console starts with.
3344
3517
  * @param {string=} options.runner
3345
3518
  * @param {string=} options.blockType
3519
+ * @param {string=} options.language
3346
3520
  * @param {(boolean|string)=} options.showResults
3347
3521
  * @param {string=} options.hidePrefix
3348
3522
  * @param {string=} options.dividerOrient