yourjs-box 1.0.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.
@@ -0,0 +1,1891 @@
1
+ /*! yourjs-box v1.0.0 | (c) 2023-present Christopher West | MIT License | https://github.com/westc/yourjs-box */
2
+ (() => {
3
+ /**
4
+ * Viewer IFRAME's CSS code
5
+ * @type {string}
6
+ */
7
+ const VIEWER_IFRAME_CSS = "[v-cloak]{display:none}body,html{height:100%;margin:0}body{background-color:var(--console-bg);font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}#vueApp{display:flex;flex-direction:column;inset:0;position:fixed}#main{display:grid;flex-grow:1;gap:0;min-height:0;position:relative}#main.is-moving-divider{-webkit-user-select:none;user-select:none}#main.col-orient.is-moving-divider{cursor:col-resize}#main.row-orient.is-moving-divider{cursor:row-resize}#main.is-moving-divider::after{background-color:var(--accent);content:'';position:absolute;z-index:99}#main.col-orient.is-moving-divider::after{bottom:0;left:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px);top:0;width:2px}#main.row-orient.is-moving-divider::after{height:2px;left:0;right:0;top:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px)}#main.col-orient{grid-template-columns:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2);grid-template-rows:1fr}#main.row-orient{grid-template-columns:1fr;grid-template-rows:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2)}.divider{background-color:var(--divider-bg);box-sizing:border-box;position:relative;transition:background-color .15s}#main.col-orient .divider{border-left:1px solid var(--toolbar-border);border-right:1px solid var(--toolbar-border);cursor:col-resize}#main.row-orient .divider{border-bottom:1px solid var(--toolbar-border);border-top:1px solid var(--toolbar-border);cursor:row-resize}.divider::after{background-color:var(--divider-grip);border-radius:2px;content:'';left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);transition:background-color .15s}#main.col-orient .divider::after{height:32px;width:2px}#main.row-orient .divider::after{height:2px;width:32px}#main.is-moving-divider .divider,.divider:hover{background-color:var(--divider-hover-bg)}#main.is-moving-divider .divider::after,.divider:hover::after{background-color:var(--accent)}#displays{position:relative}#displays>div{position:absolute;inset:0;overflow:auto}#bottomNav{align-items:center;background-color:var(--toolbar-bg);border-top:1px solid var(--toolbar-border);color:var(--toolbar-text);display:flex;flex:0 0 auto;font-size:12px;gap:8px;height:32px;padding:0 6px 0 8px;-webkit-user-select:none;user-select:none}#bottomNav>.brand{align-items:center;display:flex;flex-grow:1;gap:8px}.runner-badge{border:1px solid var(--toolbar-border);border-radius:999px;color:var(--muted-text);font-size:10px;letter-spacing:.04em;line-height:15px;padding:0 6px;text-transform:uppercase}#bottomNav>.buttons{align-items:center;display:flex;gap:2px}#bottomNav .separator{background-color:var(--toolbar-border);height:16px;margin:0 4px;width:1px}#bottomNav button{align-items:center;background:0 0;border:0;border-radius:4px;color:inherit;cursor:pointer;display:inline-flex;font:inherit;gap:5px;height:24px;justify-content:center;min-width:26px;padding:0 6px}#bottomNav button>span:not(.label){display:inline-flex;font-size:14px}#bottomNav button svg:not(.spin){transform:none!important}#bottomNav button:hover:not(:disabled){background-color:var(--button-hover)}#bottomNav button:disabled{cursor:not-allowed;opacity:.45}#bottomNav button.primary{background-color:var(--accent);color:var(--accent-text);font-weight:600;margin-left:2px;padding:0 10px 0 8px}#bottomNav button.primary>span:not(.label){font-size:11px}#bottomNav button.primary:hover:not(:disabled){background-color:var(--accent-hover)}#bottomNav button:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.spin{animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.logo{align-items:center;color:var(--toolbar-text);display:inline-flex;font-weight:700;gap:5px;letter-spacing:-.01em;line-height:1}.logo-mark{align-items:flex-end;background-color:#f7df1e;border-radius:3px;box-sizing:border-box;color:#1a1a1a;display:inline-flex;font-size:9.5px;font-weight:800;height:18px;justify-content:flex-end;letter-spacing:-.02em;padding:0 2px 2px 0;width:18px}.logo-text{font-size:14px}.logo-large{gap:12px}.logo-large>.logo-mark{border-radius:10px;box-shadow:0 8px 24px rgb(0 0 0 / .18);font-size:24px;height:56px;padding:0 6px 5px 0;width:56px}.logo-large>.logo-text{font-size:36px}#splash{align-items:center;background-color:var(--console-bg);display:flex;inset:0;justify-content:center;position:fixed;transition:opacity .35s ease,visibility .35s;z-index:1000}#splash.hidden{opacity:0;visibility:hidden}.splash-content{align-items:center;animation:splash-in .4s ease-out both;display:flex;flex-direction:column;gap:24px}.splash-progress{background-color:var(--toolbar-border);border-radius:3px;height:3px;overflow:hidden;width:140px}.splash-progress>div{animation:splash-progress 1.1s ease-in-out infinite;background-color:var(--accent);border-radius:inherit;height:100%;width:40%}@keyframes splash-in{from{opacity:0;transform:translateY(6px)}}@keyframes splash-progress{from{transform:translateX(-100%)}to{transform:translateX(250%)}}.dialog-backdrop{align-items:center;animation:fade-in .12s ease-out;background-color:var(--backdrop);display:flex;inset:0;justify-content:center;position:fixed;z-index:900}.dialog{animation:dialog-in .15s ease-out;background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;box-shadow:0 12px 40px rgb(0 0 0 / .3);color:var(--console-text);font-size:13px;padding:16px;width:min(360px,calc(100% - 32px))}.dialog-title{font-size:14px;font-weight:600;margin-bottom:6px}.dialog-message{color:var(--muted-text);line-height:1.45}.dialog-buttons{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}.dialog-button{background:0 0;border:1px solid var(--toolbar-border);border-radius:4px;color:inherit;cursor:pointer;font:inherit;height:28px;padding:0 12px}.dialog-button:hover{background-color:var(--button-hover)}.dialog-button.primary{background-color:var(--accent);border-color:var(--accent);color:var(--accent-text);font-weight:600}.dialog-button.primary:hover{background-color:var(--accent-hover)}.dialog-button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}@keyframes fade-in{from{opacity:0}}@keyframes dialog-in{from{opacity:0;transform:scale(.96)}}.rotated-90deg{transform:rotate(90deg)}.align-top{vertical-align:top!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.no-select{-webkit-user-select:none;user-select:none}:root{color-scheme:light;--console-font:ui-monospace,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;--console-bg:#fff;--console-text:#1f1f1f;--row-border:#f0f0f0;--arrow:#727272;--chevron:#9aa0a6;--entry-key:#881391;--preview-key:#5f6368;--string:#c41a16;--number:#1a1aa6;--null:#80868b;--header-bg:#f1f3f4;--header-text:#3c4043;--warn-bg:#fffbe5;--warn-border:#fff5c2;--warn-text:#5c3c00;--warn-icon:#e8a600;--error-bg:#fff0f0;--error-border:#ffd6d6;--error-text:#dc362e;--error-icon:#dc362e;--table-header-bg:#f3f3f3;--table-border:#d0d0d0;--toolbar-bg:#f3f3f3;--toolbar-border:#d6d6d6;--toolbar-text:#333;--muted-text:#5f6368;--button-hover:rgb(0 0 0 / 0.08);--accent:#1a73e8;--accent-hover:#1765cc;--accent-text:#fff;--divider-bg:#f3f3f3;--divider-hover-bg:#e8eaed;--divider-grip:#b0b0b0;--dialog-bg:#fff;--backdrop:rgb(0 0 0 / 0.25)}:root[data-theme=dark]{color-scheme:dark;--console-bg:#242424;--console-text:#e3e3e3;--row-border:#3a3a3a;--arrow:#9aa0a6;--chevron:#80868b;--entry-key:#5db0d7;--preview-key:#9aa0a6;--string:#f28b54;--number:#9980ff;--null:#8e8e8e;--header-bg:#2d2e30;--header-text:#c4c7c5;--warn-bg:#332b00;--warn-border:#665500;--warn-text:#ffd17a;--warn-icon:#ffd17a;--error-bg:#290000;--error-border:#5c0000;--error-text:#ff8080;--error-icon:#ff6b6b;--table-header-bg:#2e2e2e;--table-border:#4a4a4a;--toolbar-bg:#2b2b2b;--toolbar-border:#474747;--toolbar-text:#e3e3e3;--muted-text:#9aa0a6;--button-hover:rgb(255 255 255 / 0.1);--accent:#8ab4f8;--accent-hover:#aecbfa;--accent-text:#202124;--divider-bg:#2b2b2b;--divider-hover-bg:#333;--divider-grip:#6b6b6b;--dialog-bg:#2d2e30;--backdrop:rgb(0 0 0 / 0.5)}#displays{background-color:var(--console-bg);color:var(--console-text)}#displays>div{font-family:var(--console-font);font-size:12px;line-height:16px}.console-row{border-bottom:1px solid var(--row-border);padding:2px 8px 2px 24px;position:relative;white-space:pre-wrap;word-break:break-word}.console-row>.row-icon{left:6px;line-height:0;position:absolute;top:4px}.log-error,.log-warn{margin-top:-1px}.log-warn{background-color:var(--warn-bg);border-bottom-color:var(--warn-border);border-top:1px solid var(--warn-border);color:var(--warn-text)}.log-warn>.row-icon{color:var(--warn-icon)}.log-error{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text)}.log-error>.row-icon{color:var(--error-icon)}.code-header{background-color:var(--header-bg);border-bottom:1px solid var(--row-border);color:var(--header-text);font-weight:700;padding:2px 8px;white-space:pre-wrap}.code-header.toggleable{cursor:pointer}.code-row .copy-to-editor-button{align-items:center;background-color:var(--console-bg);border:1px solid var(--toolbar-border);border-radius:4px;color:var(--toolbar-text);cursor:pointer;display:inline-flex;height:22px;justify-content:center;opacity:0;padding:0;position:absolute;right:6px;top:2px;transition:opacity .15s;width:22px}.code-row .copy-to-editor-button:focus-visible,.code-row:hover .copy-to-editor-button{opacity:1}.code-row .copy-to-editor-button:hover{background-color:var(--toolbar-bg)}@media (hover:none){.code-row .copy-to-editor-button{opacity:.8}}.notice{color:var(--null);font-style:italic}.code-row>.row-icon{color:var(--chevron)}.code-row code,.code-row pre{background:0 0!important;font-family:var(--console-font)!important;font-size:12px!important;line-height:16px!important;padding:0!important;text-shadow:none!important}.js-value{max-width:100%;vertical-align:top}.row-content>.js-value+.js-value{margin-left:1ch}.js-value-header.expandable{cursor:default}.js-value .expansion{padding-left:12px}.js-value .loading{color:var(--null);padding-left:12px}.arrow{display:inline-block;height:10px;position:relative;width:12px}.arrow.expandable::before{border-color:transparent transparent transparent var(--arrow);border-style:solid;border-width:4px 0 4px 6px;content:'';left:2px;position:absolute;top:1px;transform-origin:3px 4px;transition:transform .1s}.arrow.expandable.expanded::before{transform:rotate(90deg)}.entry-key{color:var(--entry-key)}.entry-key.dim{opacity:.6}.t-key{color:var(--preview-key)}.t-regexp,.t-string,.t-symbol{color:var(--string)}.t-number{color:var(--number)}.t-null{color:var(--null)}.t-function{font-style:italic}.t-node{color:var(--entry-key)}.console-table{border:1px solid var(--table-border);border-collapse:collapse;margin:2px 0 4px;white-space:nowrap}.console-table td,.console-table th{border-left:1px solid var(--table-border);max-width:300px;overflow:hidden;padding:1px 4px;text-align:left;text-overflow:ellipsis}.console-table th{background-color:var(--table-header-bg);border-bottom:1px solid var(--table-border);font-weight:400}";
8
+ /**
9
+ * Viewer IFRAME's HTML code
10
+ * @type {string}
11
+ */
12
+ 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></div><div id=\"vueApp\" v-cloak><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'\" :class=\"['console-row', 'log-' + display.key]\" :title=\"display.name\"><span v-if=\"display.key === 'error'\" class=\"row-icon\"><icon name=\"consoleError\"></icon></span><span v-else-if=\"display.key === 'warn'\" class=\"row-icon\"><icon name=\"consoleWarning\"></icon></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\" :path=\"[display.logId, index]\"></js-value></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 v-model=\"jsCode\" language=\"javascript\" :theme=\"theme\" @key-combo=\"onEditorKeyCombo\" height=\"100%\"></ace-editor></div></div><div id=\"bottomNav\"><div class=\"brand\"><span class=\"logo\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span> <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=\"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>";
13
+ /**
14
+ * The URL of this script which is used to remove this script's lines from the
15
+ * stack traces of errors in window mode.
16
+ */
17
+ const OWN_URL = document.currentScript?.src ?? '';
18
+
19
+ /**
20
+ * The page's console methods before any console in window mode replaced them
21
+ * so that this script's own messages don't show up in those consoles. These
22
+ * are shared by every copy of this script that is loaded in the page.
23
+ */
24
+ const ORIGINAL_CONSOLE = window[Symbol.for('yourjs-box.originalConsole')] ??= {...console};
25
+
26
+ /**
27
+ * The only functions that may be relayed to the viewer and to the runner.
28
+ * The runner executes the user's code so anything it sends must be limited
29
+ * to these calls.
30
+ */
31
+ const RELAYABLE_FUNCS = {
32
+ viewer: ['appendLog', 'appendError', 'clearDisplays', 'onCodeRan', 'updateDescriptionFor'],
33
+ runner: ['clearLogs', 'reset', 'runCode', 'sendDescriptionFor'],
34
+ };
35
+
36
+ /**
37
+ * Relays a message to its target as long as the function being called is
38
+ * allowed.
39
+ * @param {*} data
40
+ * @param {{[target: string]: {apply: (func: string, args: any[]) => void}}} targets
41
+ */
42
+ function relayMessage(data, targets) {
43
+ const {target, func, args} = Object(data);
44
+ const targetObj = targets[target];
45
+ if (targetObj && RELAYABLE_FUNCS[target].includes(func) && Array.isArray(args)) {
46
+ targetObj.apply(func, args);
47
+ }
48
+ else {
49
+ ORIGINAL_CONSOLE.error('Unhandled message sent to main handler:', data);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Creates the runner which runs the user's code and reports what it logs. This
55
+ * function must be self-contained because in worker mode it is converted to a
56
+ * string and run inside of a Web Worker.
57
+ * @param {(message: {target: string, func: string, args: any[]}) => void} send
58
+ * Sends a message to the viewer (relayed by the main script).
59
+ * @param {Object} options
60
+ * @param {"worker"|"window"} options.mode
61
+ * "worker" if running inside of a Web Worker or "window" if running directly
62
+ * in the page that included the main script.
63
+ * @param {string=} options.ownUrl
64
+ * The URL of the script containing this function. Stack trace lines that
65
+ * refer to it are removed so that only the lines for the user's code remain.
66
+ */
67
+ function createRunner(send, options) {
68
+ const {mode} = options;
69
+ const ownUrl = options.ownUrl || (mode === 'worker' ? self.location.href : '');
70
+ const logArgsById = {};
71
+ let logCount = 0;
72
+ let snippetCount = 0;
73
+ let canImportDataUrls = true;
74
+
75
+ // Overrides for console functions. In window mode this also captures
76
+ // anything else that the page logs, just like the browser's console.
77
+ for (const key of ['clear', 'debug', 'error', 'info', 'log', 'table', 'warn']) {
78
+ const original = console[key];
79
+ if ('function' !== typeof original) continue;
80
+ console[key] = function(...args) {
81
+ if (key === 'clear') {
82
+ clearLogs();
83
+ send({target: 'viewer', func: 'clearDisplays', args: [true]});
84
+ }
85
+ else {
86
+ const table = key === 'table' ? parseTable(args[0], args[1]) : null;
87
+
88
+ // console.table() only shows the data that was tabulated.
89
+ if (table) args = args.slice(0, 1);
90
+
91
+ // Keep track of the args so that they can be expanded later.
92
+ const logId = `${++logCount}`;
93
+ logArgsById[logId] = args.map(value => ({...summarize(value), value}));
94
+
95
+ send({
96
+ target: 'viewer',
97
+ func: 'appendLog',
98
+ args: [{
99
+ logId,
100
+ key,
101
+ descriptions: logArgsById[logId].map(without(['value'])),
102
+ table,
103
+ }]
104
+ });
105
+ }
106
+
107
+ // Calls and returns the original console function.
108
+ return original.apply(this, arguments);
109
+ };
110
+ }
111
+
112
+ addEventListener('error', evt => {
113
+ reportUncaught(evt.error !== undefined ? evt.error : evt.message);
114
+ });
115
+
116
+ addEventListener('unhandledrejection', evt => {
117
+ reportUncaught(evt.reason, true);
118
+ });
119
+
120
+ /**
121
+ * @param {*} error
122
+ * @param {boolean=} isInPromise
123
+ */
124
+ function reportUncaught(error, isInPromise) {
125
+ send({
126
+ target: 'viewer',
127
+ func: 'appendError',
128
+ args: [{
129
+ message: (isInPromise ? 'Uncaught (in promise) ' : 'Uncaught ')
130
+ + cleanStack(error?.stack ?? `${error?.message ?? error}`),
131
+ }]
132
+ });
133
+ }
134
+
135
+ /**
136
+ * Removes the lines of a stack trace that refer to this runner's own code so
137
+ * that only the lines for the user's code are left.
138
+ * @param {string} stack
139
+ * @returns {string}
140
+ */
141
+ function cleanStack(stack) {
142
+ return `${stack}`
143
+ .split('\n')
144
+ .filter(line => !(ownUrl && /^\s*at\b/.test(line) && line.includes(ownUrl)))
145
+ .join('\n');
146
+ }
147
+
148
+ function without(props, obj) {
149
+ if (!obj) return obj => without(props, obj);
150
+ const ret = {...obj};
151
+ for (const prop of props) delete ret[prop];
152
+ return ret;
153
+ }
154
+
155
+ /**
156
+ * Turns the arguments passed to `console.table()` into a table similar to the
157
+ * one that the browser's console would show.
158
+ * @param {*} data
159
+ * @param {string[]=} columns
160
+ * Optional. If given only these columns will be shown.
161
+ * @return {{headers: string[], rows: {index: string, cells: (ReturnType<summarize>|null)[]}[]}|null}
162
+ * The table or `null` if `data` cannot be shown as a table.
163
+ */
164
+ function parseTable(data, columns) {
165
+ if (data === null || 'object' !== typeof data) return null;
166
+
167
+ const VALUE_HEADER = 'Value';
168
+ const headers = [];
169
+ let hasValueColumn = false;
170
+ const rowsData = Object.keys(data).map(index => {
171
+ const row = data[index];
172
+ const rowData = {index, values: {}, hasValue: false};
173
+ if (row !== null && ('object' === typeof row || 'function' === typeof row)) {
174
+ for (const key of Object.keys(row)) {
175
+ if (!headers.includes(key)) headers.push(key);
176
+ rowData.values[key] = row[key];
177
+ }
178
+ }
179
+ else {
180
+ hasValueColumn = true;
181
+ rowData.hasValue = true;
182
+ rowData.value = row;
183
+ }
184
+ return rowData;
185
+ });
186
+
187
+ // Like the browser, only show the "Value" column if no columns were given.
188
+ const shownHeaders = Array.isArray(columns) ? columns.map(c => `${c}`) : headers;
189
+ const showValueColumn = hasValueColumn && !Array.isArray(columns);
190
+ return {
191
+ headers: ['(index)', ...shownHeaders, ...(showValueColumn ? [VALUE_HEADER] : [])],
192
+ rows: rowsData.map(({index, values, hasValue, value}) => ({
193
+ index,
194
+ cells: [
195
+ ...shownHeaders.map(h => Object.hasOwn(values, h) ? summarize(values[h], 2) : null),
196
+ ...(showValueColumn ? [hasValue ? summarize(value, 2) : null] : []),
197
+ ],
198
+ })),
199
+ };
200
+ }
201
+
202
+ /**
203
+ * @param {*} value
204
+ * @returns {string}
205
+ */
206
+ function getTypeName(value) {
207
+ if (value === null || value === undefined) return '' + value;
208
+ const typeOfValue = typeof value;
209
+ // Checking for object or undefined because document.all has a type of
210
+ // undefined.
211
+ return (typeOfValue === 'object' || typeOfValue === 'undefined')
212
+ ? Object.prototype.toString.call(value).slice(8, -1)
213
+ : typeOfValue;
214
+ }
215
+
216
+ /**
217
+ * Gets the name of the class that `value` is an instance of (eg. "Person" for
218
+ * `new Person()`) falling back to `typeName` if the name can't be determined.
219
+ * @param {*} value
220
+ * @param {string} typeName
221
+ * @returns {string}
222
+ */
223
+ function getClassName(value, typeName) {
224
+ try {
225
+ const name = Object.getPrototypeOf(value)?.constructor?.name;
226
+ if (name && 'string' === typeof name) return name;
227
+ } catch (e) {}
228
+ return typeName;
229
+ }
230
+
231
+ /**
232
+ * The maximum number of properties or items shown in a preview.
233
+ */
234
+ const MAX_PREVIEW_PROPS = 5;
235
+ const MAX_PREVIEW_ITEMS = 100;
236
+
237
+ /**
238
+ * A preview is made up of parts so that the viewer can color each part like
239
+ * the browser's console does. Each part is `[kind, text]` where `kind` is one
240
+ * of "text", "key", "string", "number", "null", "symbol", "regexp" or
241
+ * "function".
242
+ * @typedef {[string, string][]} PreviewParts
243
+ */
244
+
245
+ /**
246
+ * @param {*} value
247
+ * @param {number=} depth
248
+ * Optional, defaults to `0`. `0` is used for values passed directly to a
249
+ * console function, `1` for property values shown when expanding an object
250
+ * and `2` for values nested within another value's preview.
251
+ * @param {boolean=} isPrototype
252
+ * Optional, defaults to `false`. If `true` only the name of the prototype
253
+ * will be shown (eg. "Object").
254
+ * @returns {{typeName: string, isPrimitive: boolean, parts: PreviewParts}}
255
+ */
256
+ function summarize(value, depth = 0, isPrototype = false) {
257
+ const typeName = getTypeName(value);
258
+ const isPrimitive = typeName === 'function'
259
+ ? false
260
+ : typeName === typeName.toLowerCase();
261
+ const parts = isPrototype
262
+ ? [['text', typeName !== 'Object' ? typeName : getClassName(value, typeName)]]
263
+ : getPreviewParts(value, depth, typeName);
264
+ return {typeName, isPrimitive, parts};
265
+ }
266
+
267
+ /**
268
+ * @param {*} value
269
+ * @param {number} depth
270
+ * @param {string=} typeName
271
+ * @returns {PreviewParts}
272
+ */
273
+ function getPreviewParts(value, depth, typeName = getTypeName(value)) {
274
+ if (typeName === 'string') {
275
+ if (!depth) return [['text', value]];
276
+ if (depth > 1 && value.length > 100) value = value.slice(0, 99) + '…';
277
+ return [['string', quote(value)]];
278
+ }
279
+ if (typeName === 'number') return [['number', Object.is(value, -0) ? '-0' : '' + value]];
280
+ if (typeName === 'bigint') return [['number', value + 'n']];
281
+ if (typeName === 'boolean') return [['number', '' + value]];
282
+ if (typeName === 'null' || typeName === 'undefined') return [['null', typeName]];
283
+ if (typeName === 'symbol') return [['symbol', value.toString()]];
284
+ if (typeName === 'function') return getFunctionPreviewParts(value, depth);
285
+
286
+ // Previewing can throw for objects like Map.prototype which claim to be a
287
+ // Map (via Symbol.toStringTag) but whose getters only work on a Map.
288
+ try {
289
+ return getObjectPreviewParts(value, depth, typeName);
290
+ }
291
+ catch (e) {
292
+ return [['text', typeName]];
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Quotes a string the way the browser's console does.
298
+ * @param {string} string
299
+ * @returns {string}
300
+ */
301
+ function quote(string) {
302
+ const json = JSON.stringify(string);
303
+ return string.includes("'")
304
+ ? json
305
+ : `'${json.slice(1, -1).replace(/\\"/g, '"')}'`;
306
+ }
307
+
308
+ /**
309
+ * @param {Function} value
310
+ * @param {number} depth
311
+ * @returns {PreviewParts}
312
+ */
313
+ function getFunctionPreviewParts(value, depth) {
314
+ let source = '';
315
+ try {
316
+ source = Function.prototype.toString.call(value);
317
+ } catch (e) {}
318
+ const isClass = /^class\b/.test(source);
319
+
320
+ // Like the browser, show the source of functions passed directly to a
321
+ // console function.
322
+ if (!depth) {
323
+ return [['text', isClass ? source : source.replace(/^(async\s+)?function\b\s*/, '$1ƒ ')]];
324
+ }
325
+ if (depth > 1) return [['function', 'ƒ']];
326
+ return [['function', isClass ? `class ${value.name}` : `ƒ ${value.name}()`]];
327
+ }
328
+
329
+ /**
330
+ * @param {*} value
331
+ * @param {number} depth
332
+ * @param {string} typeName
333
+ * @returns {PreviewParts}
334
+ */
335
+ function getObjectPreviewParts(value, depth, typeName) {
336
+ const className = getClassName(value, typeName);
337
+ const isArrayLike = /^Array(?:[^a-z]|$)|[^A-Z]Array$/.test(typeName);
338
+
339
+ if (typeName === 'Date') {
340
+ return [['text', isNaN(value) ? 'Invalid Date' : Date.prototype.toString.call(value)]];
341
+ }
342
+ if (typeName === 'RegExp') return [['regexp', '' + value]];
343
+ // DOM nodes (only available in window mode) are shown like the browser
344
+ // shows them in previews (eg. "h2#title.big").
345
+ if ('undefined' !== typeof Node && value instanceof Node) {
346
+ if (value.nodeType === Node.ELEMENT_NODE) {
347
+ return [['node', value.localName
348
+ + (value.id ? '#' + value.id : '')
349
+ + [...value.classList].map(c => '.' + c).join('')]];
350
+ }
351
+ if (value.nodeType === Node.TEXT_NODE) return [['string', quote(value.data)]];
352
+ return [['node', value.nodeName]];
353
+ }
354
+ if (value instanceof Error) {
355
+ return [['text', !depth ? cleanStack(value.stack ?? `${value}`) : `${value.name}: ${value.message}`]];
356
+ }
357
+
358
+ // Values nested within another preview are abbreviated.
359
+ if (depth > 1) {
360
+ if (isArrayLike) return [['text', `${className}(${value.length})`]];
361
+ if (typeName === 'Map' || typeName === 'Set') return [['text', `${className}(${value.size})`]];
362
+ return [['text', className === 'Object' ? '{…}' : className]];
363
+ }
364
+
365
+ const parts = [];
366
+ const addItems = (items, total, addItem) => {
367
+ items.forEach((item, index) => {
368
+ if (index) parts.push(['text', ', ']);
369
+ addItem(item);
370
+ });
371
+ if (total > items.length) parts.push(['text', (items.length ? ', ' : '') + '…']);
372
+ };
373
+
374
+ if (isArrayLike) {
375
+ const length = value.length;
376
+ parts.push(['text', typeName === 'Array' && className === 'Array' ? `(${length}) [` : `${className}(${length}) [`]);
377
+ addItems(
378
+ Array.from({length: Math.min(length, MAX_PREVIEW_ITEMS)}, (_, i) => i),
379
+ length,
380
+ i => parts.push(...(i in value ? getPreviewParts(value[i], 2) : [['null', 'empty']]))
381
+ );
382
+ parts.push(['text', ']']);
383
+ }
384
+ else if (typeName === 'Map' || typeName === 'Set') {
385
+ const items = [...value].slice(0, MAX_PREVIEW_ITEMS);
386
+ parts.push(['text', `${className}(${value.size}) {`]);
387
+ addItems(items, value.size, item => {
388
+ if (typeName === 'Map') {
389
+ parts.push(...getPreviewParts(item[0], 2), ['text', ' => '], ...getPreviewParts(item[1], 2));
390
+ }
391
+ else {
392
+ parts.push(...getPreviewParts(item, 2));
393
+ }
394
+ });
395
+ parts.push(['text', '}']);
396
+ }
397
+ else {
398
+ const keys = Object.keys(value);
399
+ if (className !== 'Object') parts.push(['text', className + ' ']);
400
+ if (typeName === 'Number' || typeName === 'String' || typeName === 'Boolean') {
401
+ parts.push(['text', '{'], ...getPreviewParts(value.valueOf(), 2), ['text', '}']);
402
+ }
403
+ else {
404
+ parts.push(['text', '{']);
405
+ addItems(keys.slice(0, MAX_PREVIEW_PROPS), keys.length, key => {
406
+ // Like the browser, don't call getters just to show a preview.
407
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
408
+ parts.push(
409
+ ['key', key],
410
+ ['text', ': '],
411
+ ...('value' in descriptor ? getPreviewParts(descriptor.value, 2) : [['text', '(…)']])
412
+ );
413
+ });
414
+ parts.push(['text', '}']);
415
+ }
416
+ }
417
+ return parts;
418
+ }
419
+
420
+ /**
421
+ * Describes the children of a value so that it can be expanded in the viewer.
422
+ * @param {*} value
423
+ * @param {*=} receiver
424
+ * Optional, defaults to `value`. The object used as `this` when calling
425
+ * getters. When describing a prototype this is the object that was
426
+ * originally logged so that getters (eg. `size` on `Map.prototype`) work.
427
+ */
428
+ function describe(value, receiver = value) {
429
+ const typeName = getTypeName(value);
430
+ const entries = [];
431
+ const $entries = [];
432
+ const protoEntries = [];
433
+ const $protoEntries = [];
434
+
435
+ if (value !== null && ('object' === typeof value || 'function' === typeof value)) {
436
+ let wasIterated = false;
437
+ try {
438
+ // Map
439
+ if (typeName === 'Map') {
440
+ for (const [k, v] of [...value]) {
441
+ entries.push([getPreviewParts(k, 2).map(part => part[1]).join(''), summarize(v, 1)]);
442
+ $entries.push({value: v});
443
+ }
444
+ wasIterated = true;
445
+ }
446
+ // other iterable (eg. Int8Array, Set)
447
+ else if (typeName !== 'Array' && typeName !== 'function' && 'function' === typeof value[Symbol.iterator]) {
448
+ let index = 0;
449
+ for (const v of value) {
450
+ entries.push(['' + index, summarize(v, 1)]);
451
+ $entries.push({value: v});
452
+ ++index;
453
+ }
454
+ wasIterated = true;
455
+ }
456
+ }
457
+ catch (e) {
458
+ // Prototypes such as Map.prototype look iterable but cannot be iterated
459
+ // so their properties will be listed instead.
460
+ entries.length = $entries.length = 0;
461
+ }
462
+
463
+ // Array, Object, functions, prototypes, etc.
464
+ if (!wasIterated) {
465
+ for (const k of Object.getOwnPropertyNames(value)) {
466
+ try {
467
+ const descriptor = Object.getOwnPropertyDescriptor(value, k);
468
+ const v = 'value' in descriptor ? descriptor.value : Reflect.get(value, k, receiver);
469
+ entries.push([k, summarize(v, 1), descriptor.enumerable]);
470
+ $entries.push({value: v});
471
+ } catch(e) {}
472
+ }
473
+ }
474
+
475
+ // The prototype is its own subtree which can be expanded to see its
476
+ // properties (and its own prototype).
477
+ const proto = Object.getPrototypeOf(value);
478
+ if (proto !== null) {
479
+ protoEntries.push(['[[Prototype]]', summarize(proto, 1, true)]);
480
+ $protoEntries.push({value: proto, receiver});
481
+ }
482
+ }
483
+
484
+ return {entries, protoEntries, $entries, $protoEntries};
485
+ }
486
+
487
+ // ['123', 0, 'entries', 0]
488
+ // ['123', 0, 'entries', 0]
489
+ // LOGS = {
490
+ // '123': [
491
+ // {
492
+ // value: {a: 4},
493
+ // $entries: [{value: 4}]
494
+ // }
495
+ // ]
496
+ // }
497
+
498
+ /**
499
+ * Runs the code as a classic script so that top-level declarations are
500
+ * shared between all of the code that is run.
501
+ * @param {string} jsCode
502
+ */
503
+ function runCode(jsCode) {
504
+ // Names the code so that stack traces refer to it by this name.
505
+ const source = `${jsCode}\n//# sourceURL=snippet-${++snippetCount}.js`;
506
+
507
+ if (mode === 'worker') {
508
+ try {
509
+ importCode(source);
510
+ }
511
+ catch (e) {
512
+ reportUncaught(e);
513
+ }
514
+ }
515
+ else {
516
+ const script = document.createElement('script');
517
+ script.textContent = source;
518
+ document.head.appendChild(script);
519
+ script.remove();
520
+ }
521
+
522
+ // Lets the viewer know that any synchronous logs have already been sent.
523
+ send({target: 'viewer', func: 'onCodeRan', args: []});
524
+ }
525
+
526
+ /**
527
+ * Runs code in the worker via importScripts() so that it runs as a classic
528
+ * script. A data URL is used because a worker with an opaque origin (which
529
+ * is the case when the worker itself was created from a data URL) can't
530
+ * always import blob URLs (eg. Chrome on HTTPS sites). If data URLs can't be
531
+ * imported a blob URL is used instead. Either way a failure to load means
532
+ * the code never ran, so it is safe to try again with the other kind of URL.
533
+ * @param {string} source
534
+ */
535
+ function importCode(source) {
536
+ if (canImportDataUrls) {
537
+ try {
538
+ importScripts('data:text/javascript;charset=utf-8,' + encodeURIComponent(source));
539
+ return;
540
+ }
541
+ catch (e) {
542
+ if (e?.name !== 'NetworkError') throw e;
543
+ canImportDataUrls = false;
544
+ }
545
+ }
546
+ const url = URL.createObjectURL(new Blob([source], {type: 'text/javascript'}));
547
+ try {
548
+ importScripts(url);
549
+ }
550
+ finally {
551
+ URL.revokeObjectURL(url);
552
+ }
553
+ }
554
+
555
+ /**
556
+ * @param {(string|number)[]} path
557
+ * The log ID, the argument index and then pairs of entry group keys and
558
+ * entry indices.
559
+ */
560
+ function sendDescriptionFor(path) {
561
+ // Get the description of the desired value.
562
+ let level = logArgsById;
563
+ let pathPartIndex = 0;
564
+ for (let pathPart of path) {
565
+ if (pathPartIndex && pathPartIndex % 2 === 0) {
566
+ pathPart = '$' + pathPart;
567
+ }
568
+ level = level?.[pathPart];
569
+ pathPartIndex++;
570
+ }
571
+ if (!level) return;
572
+ const description = describe(level.value, 'receiver' in level ? level.receiver : level.value);
573
+
574
+ // Change the summary to an actual description at the level found.
575
+ Object.assign(level, description);
576
+
577
+ // Send the description without values back to the viewer.
578
+ send({
579
+ target: 'viewer',
580
+ func: 'updateDescriptionFor',
581
+ args: [path, without(['$entries', '$protoEntries'], description)],
582
+ });
583
+ }
584
+
585
+ /**
586
+ * Forgets all of the logged values (eg. when the console is cleared).
587
+ */
588
+ function clearLogs() {
589
+ for (const logId of Object.keys(logArgsById)) delete logArgsById[logId];
590
+ }
591
+
592
+ return {clearLogs, runCode, sendDescriptionFor};
593
+ }
594
+
595
+
596
+ /**
597
+ * Runs the user's code in a Web Worker. The worker can be terminated which
598
+ * makes it possible to reset the console even if the code never finishes.
599
+ * @param {(message: any) => void} onMessage
600
+ */
601
+ function createWorkerRunner(onMessage) {
602
+ const WORKER_SOURCE = '(function(){'
603
+ + 'var runner = (' + createRunner + ')(function(m) { postMessage(m); }, {mode: "worker"});'
604
+ + 'onmessage = function(e) {'
605
+ + 'var d = e.data;'
606
+ + 'if (d && Object.prototype.hasOwnProperty.call(runner, d.func) && Array.isArray(d.args)) {'
607
+ + 'runner[d.func].apply(null, d.args);'
608
+ + '}'
609
+ + '};'
610
+ + 'postMessage({target: "main", func: "ready"});'
611
+ + '})();';
612
+
613
+ // A data URL gives the worker an opaque origin so that it cannot make
614
+ // requests with the page's cookies. Not all browsers support data URL
615
+ // workers so a blob URL is used as a fallback.
616
+ let canUseDataUrl = true;
617
+ let blobUrl;
618
+ let worker, isReady, pendingMessages;
619
+
620
+ function start() {
621
+ isReady = false;
622
+ pendingMessages = [];
623
+ const url = canUseDataUrl
624
+ ? 'data:text/javascript;charset=utf-8,' + encodeURIComponent(WORKER_SOURCE)
625
+ : (blobUrl ??= URL.createObjectURL(new Blob([WORKER_SOURCE], {type: 'text/javascript'})));
626
+ try {
627
+ worker = new Worker(url);
628
+ }
629
+ catch (e) {
630
+ if (!canUseDataUrl) throw e;
631
+ canUseDataUrl = false;
632
+ return start();
633
+ }
634
+
635
+ const thisWorker = worker;
636
+ worker.onmessage = e => {
637
+ if (thisWorker !== worker) return;
638
+ if (e.data?.target === 'main' && e.data.func === 'ready') {
639
+ isReady = true;
640
+ for (const message of pendingMessages.splice(0)) worker.postMessage(message);
641
+ }
642
+ else {
643
+ onMessage(e.data);
644
+ }
645
+ };
646
+ worker.onerror = e => {
647
+ // If the worker failed to start from a data URL try a blob URL.
648
+ if (thisWorker === worker && !isReady && canUseDataUrl) {
649
+ e.preventDefault();
650
+ canUseDataUrl = false;
651
+ const messages = pendingMessages;
652
+ worker.terminate();
653
+ start();
654
+ pendingMessages.push(...messages);
655
+ }
656
+ };
657
+ }
658
+
659
+ start();
660
+
661
+ return {
662
+ apply(func, args) {
663
+ if (func === 'reset') {
664
+ worker.terminate();
665
+ start();
666
+ }
667
+ else if (isReady) {
668
+ worker.postMessage({func, args});
669
+ }
670
+ else {
671
+ pendingMessages.push({func, args});
672
+ }
673
+ },
674
+ };
675
+ }
676
+
677
+ /**
678
+ * Runs the user's code directly in this window so that it has access to
679
+ * everything defined by the page.
680
+ * @param {(message: any) => void} onMessage
681
+ */
682
+ function createWindowRunner(onMessage) {
683
+ const runner = createRunner(onMessage, {mode: 'window', ownUrl: OWN_URL});
684
+ return {
685
+ apply(func, args) {
686
+ // Anything the code defined stays defined so a reset can only forget
687
+ // the logged values.
688
+ if (func === 'reset') runner.clearLogs();
689
+ else runner[func](...args);
690
+ },
691
+ };
692
+ }
693
+
694
+ /**
695
+ * Function executed when the script is included in a document.
696
+ * @param {HTMLScriptElement} script
697
+ * This is the current script but also the placeholder for where the
698
+ * console will be inserted into the DOM.
699
+ */
700
+ function main(script) {
701
+ const dataset = JSON.parse(JSON.stringify(script.dataset));
702
+ const runnerMode = dataset.runner === 'window' ? 'window' : 'worker';
703
+ dataset.runner = runnerMode;
704
+
705
+ /** @type {ReturnType<createCallableFrame>} */
706
+ let callableViewerFrame;
707
+ const sendToViewer = data => relayMessage(data, {viewer: callableViewerFrame});
708
+ const runner = runnerMode === 'window'
709
+ ? createWindowRunner(sendToViewer)
710
+ : createWorkerRunner(sendToViewer);
711
+
712
+ // The theme is determined up front so that the loading screen uses it.
713
+ const theme = /^(light|dark)$/.test(dataset.theme)
714
+ ? dataset.theme
715
+ : matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
716
+
717
+ callableViewerFrame = createCallableFrame({
718
+ jsCode() {
719
+ let mountedApp;
720
+
721
+ const Prism = window.Prism;
722
+ delete window.Prism;
723
+ Prism.plugins.autoloader.loadLanguages('javascript');
724
+
725
+ /**
726
+ * The minimum amount of time (in milliseconds) that the loading screen is shown.
727
+ */
728
+ const MIN_LOADING_TIME = 1000;
729
+
730
+ /**
731
+ * If the orientation isn't specified, the editor is shown below the console
732
+ * instead of beside it when the console is narrower than this (in pixels).
733
+ */
734
+ const NARROW_WIDTH = 600;
735
+
736
+ /**
737
+ * @returns {"horizontal"|"vertical"}
738
+ * The orientation to use when one wasn't specified.
739
+ */
740
+ function getAutoDividerOrient() {
741
+ return innerWidth < NARROW_WIDTH ? 'horizontal' : 'vertical';
742
+ }
743
+
744
+ function init(jsCode, dataset) {
745
+ const hidePrefix = dataset.hidePrefix ?? '';
746
+ const darkSchemeQuery = matchMedia('(prefers-color-scheme: dark)');
747
+ const {visibleCode, hiddenGroups} = extractHiddenGroups(unindentMin(jsCode), hidePrefix);
748
+ const copyHiddenGroups = () => hiddenGroups.map(group => ({...group}));
749
+
750
+ mountedApp = Vue
751
+ .createApp({
752
+ data() {
753
+ return {
754
+ displays: [],
755
+ hidePrefix,
756
+ hiddenGroups: copyHiddenGroups(),
757
+ runnerMode: dataset.runner,
758
+ // The number of groups of code sent to the runner that haven't
759
+ // finished running yet.
760
+ runningCount: 0,
761
+ /** @type {{title: string, message: string, confirmText: string, resolve: (isConfirmed: boolean) => void}?} */
762
+ dialog: null,
763
+ isDisplaysScrolledToBottom: true,
764
+ // Like the browser's dev tools, follow the system's color scheme
765
+ // unless a theme was specified.
766
+ forcedTheme: /^(light|dark)$/.test(dataset.theme) ? dataset.theme : null,
767
+ prefersDark: darkSchemeQuery.matches,
768
+ runCount: 0,
769
+ jsCode: visibleCode,
770
+ // If the orientation wasn't specified it follows the width of the
771
+ // console until the user chooses one.
772
+ isDividerOrientAuto: !/^(horizontal|vertical)$/.test(dataset.dividerOrient),
773
+ dividerOrient: /^(horizontal|vertical)$/.test(dataset.dividerOrient)
774
+ ? dataset.dividerOrient
775
+ : getAutoDividerOrient(),
776
+ isMovingDivider: false,
777
+ dividerPct: '50%',
778
+ dividerSize: '8px',
779
+ tempDividerPct: null,
780
+ };
781
+ },
782
+ computed: {
783
+ theme() {
784
+ return this.forcedTheme ?? (this.prefersDark ? 'dark' : 'light');
785
+ },
786
+ bottomButtons() {
787
+ const isMac = /Mac|iPhone|iPad/.test(navigator.platform);
788
+ return [
789
+ {
790
+ iconName: 'clear',
791
+ title: 'Clear console',
792
+ callback() { this.clearConsole(); },
793
+ },
794
+ {
795
+ iconName: 'refresh',
796
+ title: this.runnerMode === 'worker'
797
+ ? 'Reset (also stops any code that is still running)'
798
+ : 'Reset',
799
+ callback() { this.resetConsole(); },
800
+ },
801
+ { isSeparator: true },
802
+ {
803
+ iconName: 'horizontalView',
804
+ title: 'Show the editor below the console',
805
+ callback() { this.setDividerOrient('horizontal'); },
806
+ showIf() { return this.dividerOrient !== 'horizontal'; }
807
+ },
808
+ {
809
+ iconName: 'verticalView',
810
+ title: 'Show the editor beside the console',
811
+ callback() { this.setDividerOrient('vertical'); },
812
+ showIf() { return this.dividerOrient !== 'vertical'; }
813
+ },
814
+ {
815
+ iconName: this.runningCount ? 'spinner' : 'play',
816
+ label: 'Run',
817
+ className: 'primary',
818
+ title: `Run the next block of code (${isMac ? '\u2318' : 'Ctrl+'}Enter)`,
819
+ callback() { this.runCode(); },
820
+ disableIf() { return !this.canRunCode; }
821
+ },
822
+ ].filter(btn => !btn.showIf || btn.showIf.call(this));
823
+ },
824
+ canRunCode() {
825
+ return this.jsCode.trim();
826
+ },
827
+ mainElemClassNames() {
828
+ return [
829
+ this.dividerOrient === 'vertical' ? 'col-orient' : 'row-orient',
830
+ this.isMovingDivider ? 'is-moving-divider' : ''
831
+ ].join(' ');
832
+ },
833
+ mainElemStyles() {
834
+ return {
835
+ '--divider-size': this.dividerSize,
836
+ '--editor-pct': this.dividerPct,
837
+ '--temp-editor-pct': this.tempDividerPct
838
+ };
839
+ }
840
+ },
841
+ methods: {
842
+ /**
843
+ * @param {KeyboardEvent} evt
844
+ */
845
+ onEditorKeyCombo(evt) {
846
+ if ((evt.ctrlKey || evt.metaKey) && evt.key === 'Enter' && this.canRunCode) {
847
+ this.runCode();
848
+ }
849
+ },
850
+ runCode() {
851
+ // Always show the output of code that is run.
852
+ this.isDisplaysScrolledToBottom = true;
853
+
854
+ const [jsCodeGroup0, ...otherJsCodeGroups] = parseJSCodeGroups(this.jsCode);
855
+ this.runGroup(jsCodeGroup0);
856
+
857
+ // Remove the code that was run from the editor.
858
+ this.jsCode = otherJsCodeGroups.map(g => g.allLines).join('\n');
859
+
860
+ this.runCount++;
861
+ },
862
+ /**
863
+ * Runs the next hidden group if all of the visible code that came
864
+ * before it has already been run. This is called again once the runner
865
+ * finishes running each group so that the displays stay in order.
866
+ */
867
+ runHiddenGroups() {
868
+ const {hiddenGroups} = this;
869
+ if (hiddenGroups.length && hiddenGroups[0].runCount <= this.runCount) {
870
+ this.runGroup(hiddenGroups.shift());
871
+ }
872
+ },
873
+ /**
874
+ * Adds a group of code to the displays and then runs it.
875
+ * @param {ReturnType<parseJSCodeGroups>[number]} group
876
+ */
877
+ runGroup(group) {
878
+ const {header, isHidden} = parseGroupHeader(group.headerLines, this.hidePrefix);
879
+ this.displays.push({
880
+ type: 'prism',
881
+ header,
882
+ isHidden,
883
+ isCodeShown: !isHidden,
884
+ value: group.lines,
885
+ });
886
+ this.runningCount++;
887
+ messageParent({target: 'runner', func: 'runCode', args: [group.lines]});
888
+ },
889
+ clearConsole() {
890
+ this.displays = [];
891
+ messageParent({target: 'runner', func: 'clearLogs', args: []});
892
+ },
893
+ async resetConsole() {
894
+ const isConfirmed = await this.confirm({
895
+ title: 'Reset the console?',
896
+ message: 'The output will be cleared and the editor will go back to the original code.'
897
+ + (this.runnerMode === 'window'
898
+ ? ' Anything the code already defined on the page will stay defined.'
899
+ : ''),
900
+ confirmText: 'Reset',
901
+ });
902
+ if (!isConfirmed) return;
903
+
904
+ this.displays = [];
905
+ this.jsCode = visibleCode;
906
+ this.hiddenGroups = copyHiddenGroups();
907
+ this.runCount = 0;
908
+ this.runningCount = 0;
909
+ messageParent({target: 'runner', func: 'reset', args: []});
910
+ this.runHiddenGroups();
911
+ },
912
+ /**
913
+ * Copies code that was already run into the editor so that it can be run
914
+ * again (as is or modified), first asking before replacing any code that
915
+ * is in the editor.
916
+ * @param {string} code
917
+ */
918
+ async copyToEditor(code) {
919
+ if (this.jsCode.trim() && this.jsCode !== code) {
920
+ const isConfirmed = await this.confirm({
921
+ title: 'Replace the code in the editor?',
922
+ message: 'The code that is currently in the editor will be replaced with a copy of the code you selected.',
923
+ confirmText: 'Replace',
924
+ });
925
+ if (!isConfirmed) return;
926
+ }
927
+ this.jsCode = code;
928
+ },
929
+ /**
930
+ * Shows a dialog asking the user to confirm something.
931
+ * @param {{title: string, message: string, confirmText: string}} options
932
+ * @returns {Promise<boolean>}
933
+ */
934
+ confirm(options) {
935
+ this.dialog?.resolve(false);
936
+ return new Promise(resolve => {
937
+ this.dialog = {...options, resolve};
938
+ this.$nextTick(() => this.$refs.dialogConfirmButton?.focus());
939
+ });
940
+ },
941
+ closeDialog(isConfirmed) {
942
+ const {dialog} = this;
943
+ this.dialog = null;
944
+ dialog?.resolve(isConfirmed);
945
+ },
946
+ /**
947
+ * Sets the orientation chosen by the user which stops it from
948
+ * automatically following the width of the console.
949
+ * @param {"horizontal"|"vertical"} orient
950
+ */
951
+ setDividerOrient(orient) {
952
+ this.isDividerOrientAuto = false;
953
+ this.dividerOrient = orient;
954
+ },
955
+ onWindowResize() {
956
+ if (this.isDividerOrientAuto) {
957
+ this.dividerOrient = getAutoDividerOrient();
958
+ }
959
+ },
960
+ onDisplaysScroll() {
961
+ const {scrollTop, scrollHeight, clientHeight} = this.$refs.displaysScroller;
962
+ this.isDisplaysScrolledToBottom = scrollHeight - scrollTop - clientHeight < 8;
963
+ },
964
+ getEditorPct(evt) {
965
+ const rect = this.$refs.main.getBoundingClientRect();
966
+ const {dividerOrient} = this;
967
+ const x = evt.pageX - rect.left;
968
+ const y = evt.pageY - rect.top;
969
+ const w = rect.width;
970
+ const h = rect.height;
971
+ const pct = 100 - 100 * Math.min(
972
+ Math.max(
973
+ 0.2,
974
+ dividerOrient === 'vertical' ? x / w : (y / h)
975
+ ),
976
+ 0.8
977
+ );
978
+ return pct + '%';
979
+ },
980
+ onMainElemMouseDown(evt) {
981
+ if (evt.target === this.$refs.mainDivider) {
982
+ this.isMovingDivider = true;
983
+ this.tempDividerPct = this.getEditorPct(evt);
984
+ }
985
+ },
986
+ onWindowMouseMove(evt) {
987
+ if (this.isMovingDivider) {
988
+ this.tempDividerPct = this.getEditorPct(evt);
989
+ }
990
+ },
991
+ onWindowMouseUp(evt) {
992
+ if (this.isMovingDivider) {
993
+ this.isMovingDivider = false;
994
+ this.dividerPct = this.getEditorPct(evt);
995
+ }
996
+ },
997
+ onWindowError(evt) {
998
+ this.displays.push({
999
+ type: 'error',
1000
+ message: evt.error?.stack ?? evt.message ?? evt.error?.message ?? `${evt.error}`,
1001
+ line: evt.lineno,
1002
+ column: evt.colno,
1003
+ })
1004
+ }
1005
+ },
1006
+ watch: {
1007
+ theme: {
1008
+ handler(theme) {
1009
+ document.documentElement.dataset.theme = theme;
1010
+ },
1011
+ immediate: true,
1012
+ },
1013
+ // Like the browser's console, keep showing the newest output unless the
1014
+ // user has scrolled up to look at older output.
1015
+ 'displays.length'() {
1016
+ if (this.isDisplaysScrolledToBottom) {
1017
+ this.$nextTick(() => {
1018
+ const scroller = this.$refs.displaysScroller;
1019
+ scroller.scrollTop = scroller.scrollHeight;
1020
+ });
1021
+ }
1022
+ },
1023
+ },
1024
+ mounted() {
1025
+ addEventListener('mousemove', this.onWindowMouseMove);
1026
+ addEventListener('resize', this.onWindowResize);
1027
+ addEventListener('mouseup', this.onWindowMouseUp);
1028
+ addEventListener('error', this.onWindowError);
1029
+ darkSchemeQuery.addEventListener('change', e => this.prefersDark = e.matches);
1030
+
1031
+ // Hidden code that came before all visible code gets run immediately.
1032
+ this.runHiddenGroups();
1033
+
1034
+ // Shows the loading screen for at least a moment and then fades it out.
1035
+ setTimeout(
1036
+ () => document.querySelector('#splash').classList.add('hidden'),
1037
+ Math.max(0, MIN_LOADING_TIME - performance.now())
1038
+ );
1039
+ }
1040
+ })
1041
+ .component('ace-editor', getAceComponentProps())
1042
+ .component('prism', getPrismComponentProps())
1043
+ .component('icon', getIconComponentProps())
1044
+ .component('js-value', getJSValueComponentProps())
1045
+ .mount(document.querySelector('#vueApp'));
1046
+ }
1047
+
1048
+ function getAceComponentProps() {
1049
+ return {
1050
+ data() {
1051
+ return {
1052
+ annotations: [],
1053
+ };
1054
+ },
1055
+ props: [
1056
+ 'height',
1057
+ 'keybinding',
1058
+ 'language',
1059
+ 'modelValue',
1060
+ 'theme',
1061
+ 'width',
1062
+ ],
1063
+ computed: {
1064
+ infoNotes() {
1065
+ return this.annotations.filter(({type}) => type === 'info');
1066
+ },
1067
+ warningNotes() {
1068
+ return this.annotations.filter(({type}) => type === 'warning');
1069
+ },
1070
+ errorNotes() {
1071
+ return this.annotations.filter(({type}) => type === 'error');
1072
+ },
1073
+ style() {
1074
+ return {
1075
+ height: this.height != null
1076
+ ? 'number' === typeof this.height
1077
+ ? this.height + 'px'
1078
+ : this.height
1079
+ : '150px',
1080
+ width: this.width != null
1081
+ ? 'number' === typeof this.width
1082
+ ? this.width + 'px'
1083
+ : this.width
1084
+ : '100%',
1085
+ };
1086
+ },
1087
+ modeSig() {
1088
+ return `ace/mode/${this.language ?? 'text'}`;
1089
+ },
1090
+ themeSig() {
1091
+ let theme = `${this.theme ?? 'light'}`.replace(
1092
+ /^light$|(^dark$)/i,
1093
+ (_, isDark) => `cloud_editor${isDark ? '_dark' : ''}`
1094
+ );
1095
+ return `ace/theme/${theme}`;
1096
+ }
1097
+ },
1098
+ watch: {
1099
+ themeSig(newValue) {
1100
+ this.editor.setTheme(newValue);
1101
+ },
1102
+ modelValue(newValue) {
1103
+ if (newValue !== this.editor.getValue()) {
1104
+ // -1 puts the cursor at the start instead of selecting everything.
1105
+ this.editor.setValue(newValue, -1);
1106
+ }
1107
+ }
1108
+ },
1109
+ async mounted() {
1110
+ // Get the ace editor set up with the language and theme.
1111
+ const editor = ace.edit(this.$refs.editor, {
1112
+ value: this.modelValue,
1113
+ mode: this.modeSig,
1114
+ theme: this.themeSig,
1115
+ });
1116
+ this.editor = editor;
1117
+
1118
+ // Use VSCode keybinding but remove the CTRL+ENTER and CTRL+SHIFT+ENTER
1119
+ // (or on Mac CMD+ENTER and CMD+SHIFT+ENTER) commands so that they can be
1120
+ // caught by custom code.
1121
+ editor.setKeyboardHandler('ace/keyboard/vscode');
1122
+ editor.commands.removeCommand('addLineAfter');
1123
+ editor.commands.removeCommand('addLineBefore');
1124
+
1125
+ // When the value in the ace editor is updated make sure to emit the
1126
+ // modelValue update event.
1127
+ editor.on('change', () => this.$emit('update:modelValue', editor.getValue()));
1128
+
1129
+ // When the annotations change in the editor go ahead and trigger the
1130
+ // different note events if necessary.
1131
+ editor.session.on("changeAnnotation", () => {
1132
+ const oldErrorsJSON = JSON.stringify(this.errorNotes);
1133
+ const oldInfosJSON = JSON.stringify(this.infoNotes);
1134
+ const oldWarningsJSON = JSON.stringify(this.warningNotes);
1135
+ this.annotations = editor.getSession().getAnnotations();
1136
+ const {errorNotes, infoNotes, warningNotes} = this;
1137
+ if (oldErrorsJSON !== JSON.stringify(errorNotes)) {
1138
+ this.$emit('changeErrorNotes', errorNotes);
1139
+ }
1140
+ if (oldInfosJSON !== JSON.stringify(infoNotes)) {
1141
+ this.$emit('changeInfoNotes', infoNotes);
1142
+ }
1143
+ if (oldWarningsJSON !== JSON.stringify(warningNotes)) {
1144
+ this.$emit('changeWarningNotes', warningNotes);
1145
+ }
1146
+ });
1147
+
1148
+ // Attempt to capture key combos.
1149
+ editor.textInput.getElement().addEventListener('keydown', (evt) => {
1150
+ const isComboKeyCode = evt.keyCode === 16 // SHIFT
1151
+ || evt.keyCode === 17 // CTRL
1152
+ || evt.keyCode === 18 // ALT or OPT
1153
+ || evt.keyCode === 91 // META
1154
+ || evt.keyCode === 92 // META
1155
+ || evt.keyCode === 93 // META;
1156
+
1157
+ // Dont do anything if only a combo key is being pressed.
1158
+ if (isComboKeyCode) return;
1159
+
1160
+ // If this is really a key combo then go ahead and emit that event.
1161
+ const isCombo = evt.metaKey
1162
+ || evt.ctrlKey
1163
+ || evt.altKey
1164
+ || (evt.shiftKey && (evt.key ?? '').length > 1);
1165
+ if (isCombo) this.$emit('keyCombo', evt);
1166
+ });
1167
+ },
1168
+ template: '<div ref="editor" :style="style"></div>',
1169
+ };
1170
+ }
1171
+
1172
+ function getPrismComponentProps() {
1173
+ return {
1174
+ data() {
1175
+ return {
1176
+ annotations: [],
1177
+ };
1178
+ },
1179
+ props: ['code', 'isDark', 'language', 'lineNumbers', 'matchBraces'],
1180
+ computed: {
1181
+ html() {},
1182
+ preStyle() {
1183
+ return {
1184
+ filter: (this.isDark ?? false) !== false
1185
+ ? 'none'
1186
+ : 'invert(1) hue-rotate(180deg) brightness(1.1)',
1187
+ margin: 0,
1188
+ paddingTop: '0.5em',
1189
+ paddingBottom: '0.5em'
1190
+ };
1191
+ }
1192
+ },
1193
+ watch: {
1194
+ code() { this.redraw(); },
1195
+ language() { this.redraw(); },
1196
+ lineNumbers() { this.redraw(); },
1197
+ matchBraces() { this.redraw(); },
1198
+ },
1199
+ methods: {
1200
+ async redraw() {
1201
+ /** @type {HTMLPreElement} */
1202
+ const preElem = this.$refs.pre;
1203
+ /** @type {HTMLElement} */
1204
+ const codeElem = this.$refs.code;
1205
+
1206
+ codeElem.textContent = this.code;
1207
+
1208
+ codeElem.className = `language-javascript`;
1209
+ preElem.className = '';
1210
+
1211
+ for (const propName of ['lineNumbers', 'matchBraces']) {
1212
+ if (this[propName] != null) {
1213
+ const dashedPropName = propName.replace(/[A-Z]+/g, '-$&').toLowerCase();
1214
+ preElem.className += ' ' + dashedPropName;
1215
+ }
1216
+ }
1217
+
1218
+ // Wait for all scripts to load before trying to syntax highlight the
1219
+ // code element.
1220
+ Prism.highlightElement(codeElem);
1221
+ }
1222
+ },
1223
+ async mounted() {
1224
+ await this.redraw();
1225
+ },
1226
+ template: '<pre ref="pre" :style="preStyle"><code ref="code"></code></pre>',
1227
+ };
1228
+ }
1229
+
1230
+ function getIconComponentProps() {
1231
+ return {
1232
+ props: ['name'],
1233
+ computed: {
1234
+ svgCode() {
1235
+ const codes = {
1236
+ // https://icon-sets.iconify.design/carbon/unknown/
1237
+ missing: '<svg viewBox="0 0 32 32"><circle cx="16" cy="22.5" r="1.5" fill="currentColor"/><path fill="currentColor" d="M17 19h-2v-4h2c1.103 0 2-.897 2-2s-.897-2-2-2h-2c-1.103 0-2 .897-2 2v.5h-2V13c0-2.206 1.794-4 4-4h2c2.206 0 4 1.794 4 4s-1.794 4-4 4z"/><path fill="currentColor" d="M29.391 14.527L17.473 2.609A2.078 2.078 0 0 0 16 2c-.533 0-1.067.203-1.473.609L2.609 14.527C2.203 14.933 2 15.466 2 16s.203 1.067.609 1.473L14.526 29.39c.407.407.941.61 1.474.61s1.067-.203 1.473-.609L29.39 17.474c.407-.407.61-.94.61-1.474s-.203-1.067-.609-1.473M16 28.036L3.965 16L16 3.964L28.036 16z"/></svg>',
1238
+ // https://icon-sets.iconify.design/radix-icons/trash/
1239
+ trash: '<svg viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M5.5 1a.5.5 0 0 0 0 1h4a.5.5 0 0 0 0-1zM3 3.5a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 0 1H11v8a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4h-.5a.5.5 0 0 1-.5-.5M5 4h5v8H5z" clip-rule="evenodd"/></svg>',
1240
+ // https://icon-sets.iconify.design/radix-icons/play/
1241
+ play: '<svg viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M3.242 2.322a.5.5 0 0 1 .491-.014l9 4.75a.5.5 0 0 1 0 .884l-9 4.75A.5.5 0 0 1 3 12.25v-9.5a.5.5 0 0 1 .242-.428M4 3.579v7.842L11.429 7.5z" clip-rule="evenodd"/></svg>',
1242
+ // https://icon-sets.iconify.design/radix-icons/update/
1243
+ refresh: '<svg viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M1.903 7.297c0 3.044 2.207 5.118 4.686 5.547a.521.521 0 1 1-.178 1.027C3.5 13.367.861 10.913.861 7.297c0-1.537.699-2.745 1.515-3.663c.585-.658 1.254-1.193 1.792-1.602H2.532a.5.5 0 0 1 0-1h3a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-1 0V2.686l-.001.002c-.572.43-1.27.957-1.875 1.638c-.715.804-1.253 1.776-1.253 2.97m11.108.406c0-3.012-2.16-5.073-4.607-5.533a.521.521 0 1 1 .192-1.024c2.874.54 5.457 2.98 5.457 6.557c0 1.537-.699 2.744-1.515 3.663c-.585.658-1.254 1.193-1.792 1.602h1.636a.5.5 0 1 1 0 1h-3a.5.5 0 0 1-.5-.5v-3a.5.5 0 1 1 1 0v1.845h.002c.571-.432 1.27-.958 1.874-1.64c.715-.803 1.253-1.775 1.253-2.97" clip-rule="evenodd"/></svg>',
1244
+ // https://icon-sets.iconify.design/radix-icons/view-horizontal/
1245
+ horizontalView: '<svg viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M1.5 2h12a.5.5 0 0 1 .5.5V7H1V2.5a.5.5 0 0 1 .5-.5M1 8v4.5a.5.5 0 0 0 .5.5h12a.5.5 0 0 0 .5-.5V8zM0 2.5A1.5 1.5 0 0 1 1.5 1h12A1.5 1.5 0 0 1 15 2.5v10a1.5 1.5 0 0 1-1.5 1.5h-12A1.5 1.5 0 0 1 0 12.5z" clip-rule="evenodd"/></svg>',
1246
+ // https://icon-sets.iconify.design/radix-icons/view-vertical/
1247
+ verticalView: '<svg viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M8 2h5.5a.5.5 0 0 1 .5.5v10a.5.5 0 0 1-.5.5H8zM7 2H1.5a.5.5 0 0 0-.5.5v10a.5.5 0 0 0 .5.5H7zm-7 .5A1.5 1.5 0 0 1 1.5 1h12A1.5 1.5 0 0 1 15 2.5v10a1.5 1.5 0 0 1-1.5 1.5h-12A1.5 1.5 0 0 1 0 12.5z" clip-rule="evenodd"/></svg>',
1248
+ // https://icon-sets.iconify.design/vaadin/play/
1249
+ play: '<svg viewBox="0 0 16 16"><path fill="currentColor" d="M2 1v14l12-7z"/></svg>',
1250
+ // https://icon-sets.iconify.design/mdi/error-outline/
1251
+ error: '<svg viewBox="0 0 24 24"><path fill="currentColor" d="M11 15h2v2h-2zm0-8h2v6h-2zm1-5C6.47 2 2 6.5 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2m0 18a8 8 0 0 1-8-8a8 8 0 0 1 8-8a8 8 0 0 1 8 8a8 8 0 0 1-8 8"/></svg>',
1252
+ // Icons similar to those in the browser's console.
1253
+ clear: '<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M3.75 12.25l8.5-8.5" stroke="currentColor" stroke-width="1.5"/></svg>',
1254
+ copyToEditor: '<svg viewBox="0 0 16 16"><path d="M6 3.5L2.5 7 6 10.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 7h6.5a4 4 0 0 1 4 4v1.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>',
1255
+ spinner: '<svg viewBox="0 0 16 16" class="spin"><path d="M8 1.75a6.25 6.25 0 1 1-6.25 6.25" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round"/></svg>',
1256
+ chevron: '<svg viewBox="0 0 16 16"><path d="M6 4l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
1257
+ consoleError: '<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" fill="currentColor"/><path d="M5.5 5.5l5 5m0-5l-5 5" stroke="#fff" stroke-width="1.75" stroke-linecap="round"/></svg>',
1258
+ consoleWarning: '<svg viewBox="0 0 16 16"><path d="M8 1.75L14.75 14H1.25z" fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M8 6v3.5" stroke="#202124" stroke-width="1.75" stroke-linecap="round"/><circle cx="8" cy="11.75" r="1" fill="#202124"/></svg>',
1259
+ };
1260
+ return (codes[this.name] ?? codes.missing).replace('<svg', '$& xmlns="http://www.w3.org/2000/svg" style="height: 1em; width: 1em; display: inline-block; transform: translateY(0.1em);"');
1261
+ }
1262
+ },
1263
+ template: '<span v-html="svgCode"></span>'
1264
+ }
1265
+ }
1266
+
1267
+ function getJSValueComponentProps() {
1268
+ return {
1269
+ props: ['description', 'path', 'name', 'isDimName'],
1270
+ data() {
1271
+ return {
1272
+ isExpanded: false,
1273
+ hasBeenExpanded: false,
1274
+ };
1275
+ },
1276
+ watch: {
1277
+ isExpanded(newValue) {
1278
+ if (newValue && !this.hasBeenExpanded) {
1279
+ this.hasBeenExpanded = newValue;
1280
+ }
1281
+ }
1282
+ },
1283
+ computed: {
1284
+ isPartialDescription() {
1285
+ return this.description.entries === undefined;
1286
+ },
1287
+ isExpandable() {
1288
+ return !this.description.isPrimitive;
1289
+ },
1290
+ isEntry() {
1291
+ return this.name != null;
1292
+ },
1293
+ classNames() {
1294
+ return [
1295
+ 'js-value',
1296
+ // Entries are always on their own line just like expanded values.
1297
+ (this.isEntry || (this.isExpandable && this.isExpanded)) ? 'd-block' : 'd-inline-block',
1298
+ this.isExpanded ? 'expanded' : '',
1299
+ ];
1300
+ },
1301
+ entryGroups() {
1302
+ // protoEntries only ever contains the [[Prototype]] entry.
1303
+ return [
1304
+ { key: 'entries', isDim: entry => entry[2] === false },
1305
+ { key: 'protoEntries', isDim: () => true },
1306
+ ];
1307
+ }
1308
+ },
1309
+ methods: {
1310
+ toggleExpanded() {
1311
+ if (!this.isExpandable) return;
1312
+ this.isExpanded = !this.isExpanded;
1313
+ if (this.isExpanded && this.isPartialDescription) {
1314
+ messageParent({target: 'runner', func: 'sendDescriptionFor', args: [this.path]});
1315
+ }
1316
+ }
1317
+ },
1318
+ template: `
1319
+ <div :class="classNames">
1320
+ <div :class="['js-value-header', isExpandable ? 'expandable' : '']" @click="toggleExpanded"><span
1321
+ v-if="isExpandable || isEntry" :class="['arrow', isExpandable ? 'expandable' : '', isExpanded ? 'expanded' : '']"></span><template
1322
+ v-if="isEntry"><span :class="['entry-key', isDimName ? 'dim' : '']">{{ name }}</span>: </template><span
1323
+ v-for="part in description.parts" :class="'t-' + part[0]">{{ part[1] }}</span></div>
1324
+ <div v-if="hasBeenExpanded" v-show="isExpanded" class="expansion">
1325
+ <div v-if="isPartialDescription" class="loading">Loading&hellip;</div>
1326
+ <template v-else>
1327
+ <template v-for="group in entryGroups">
1328
+ <js-value
1329
+ v-for="(entry, entryIndex) in description[group.key]"
1330
+ :name="entry[0]"
1331
+ :is-dim-name="group.isDim(entry)"
1332
+ :description="entry[1]"
1333
+ :path="path.concat([group.key, entryIndex])">
1334
+ </js-value>
1335
+ </template>
1336
+ </template>
1337
+ </div>
1338
+ </div>
1339
+ `
1340
+ };
1341
+ }
1342
+
1343
+ /**
1344
+ * @param {string} jsCode
1345
+ * @returns {{headerLines: string, lines: string, allLines: string}[]}
1346
+ */
1347
+ function parseJSCodeGroups(jsCode) {
1348
+ return jsCode.split(/\r\n|\r|\n/).reduce(
1349
+ (groups, line) => {
1350
+ const lastGroup = groups[groups.length - 1];
1351
+ const isHeaderLine = /^\/\/[^]*\\\\$/.test(line);
1352
+ const headerLine = isHeaderLine ? line.replace(/^\/+\s*|\s*\\+$/g, '') : line;
1353
+ if (!groups.length || (isHeaderLine && !lastGroup.canAppendHeader)) {
1354
+ groups.push({
1355
+ headerLines: isHeaderLine ? [headerLine] : [],
1356
+ lines: isHeaderLine ? [] : [line],
1357
+ allLines: [line],
1358
+ canAppendHeader: isHeaderLine,
1359
+ });
1360
+ }
1361
+ else if (isHeaderLine) {
1362
+ lastGroup.headerLines.push(headerLine);
1363
+ lastGroup.allLines.push(line);
1364
+ }
1365
+ else {
1366
+ lastGroup.canAppendHeader = false;
1367
+ lastGroup.lines.push(line);
1368
+ lastGroup.allLines.push(line);
1369
+ }
1370
+ return groups;
1371
+ },
1372
+ []
1373
+ ).reduce(
1374
+ (groups, group) => {
1375
+ if (!group.canAppendHeader) {
1376
+ delete group.canAppendHeader;
1377
+ group.headerLines = group.headerLines.join('\n').trimEnd();
1378
+ group.lines = group.lines.join('\n');
1379
+ group.allLines = group.allLines.join('\n');
1380
+ groups.push(group);
1381
+ }
1382
+ return groups;
1383
+ },
1384
+ []
1385
+ );
1386
+ }
1387
+
1388
+ /**
1389
+ * Determines the header that should be displayed for a group of code and
1390
+ * whether or not the group's code should be hidden. A group is hidden if its
1391
+ * header starts with `hidePrefix`, in which case anything after the prefix (and
1392
+ * an optional colon) is used as the header.
1393
+ * @param {string} headerLines
1394
+ * @param {string} hidePrefix
1395
+ * @returns {{header: string, isHidden: boolean}}
1396
+ */
1397
+ function parseGroupHeader(headerLines, hidePrefix) {
1398
+ const isHidden = !!hidePrefix && headerLines.startsWith(hidePrefix);
1399
+ return {
1400
+ header: isHidden
1401
+ ? headerLines.slice(hidePrefix.length).replace(/^\s*:?\s*/, '')
1402
+ : headerLines,
1403
+ isHidden,
1404
+ };
1405
+ }
1406
+
1407
+ /**
1408
+ * Separates the hidden groups of code from the visible code.
1409
+ * @param {string} jsCode
1410
+ * @param {string} hidePrefix
1411
+ * @returns {{
1412
+ * visibleCode: string,
1413
+ * hiddenGroups: (ReturnType<parseJSCodeGroups>[number] & {runCount: number})[]
1414
+ * }}
1415
+ * `visibleCode` is the code to show in the editor. Each hidden group has a
1416
+ * `runCount` indicating how many visible groups must be run before it runs.
1417
+ */
1418
+ function extractHiddenGroups(jsCode, hidePrefix) {
1419
+ const visibleGroups = [];
1420
+ const hiddenGroups = [];
1421
+ for (const group of parseJSCodeGroups(jsCode)) {
1422
+ if (parseGroupHeader(group.headerLines, hidePrefix).isHidden) {
1423
+ hiddenGroups.push({...group, runCount: visibleGroups.length});
1424
+ }
1425
+ else {
1426
+ visibleGroups.push(group);
1427
+ }
1428
+ }
1429
+ return {
1430
+ visibleCode: visibleGroups.map(g => g.allLines).join('\n'),
1431
+ hiddenGroups,
1432
+ };
1433
+ }
1434
+
1435
+ /**
1436
+ * Finds the minimum indentation of all of the lines that have non-space
1437
+ * characters and removes the indentation accordingly for all indented lines.
1438
+ * @param {string} text
1439
+ * The string containing the lines of text that should be unindented.
1440
+ * @param {{trim: boolean, tabSize: number}=} opt_options
1441
+ * Optional, defaults to `{trim: true, tabSize: 4}`. The `trim` property
1442
+ * indicates if leading lines should be removed along with trailing
1443
+ * whitespaces. The `tabSize` property indicates how many spaces will be
1444
+ * used to replace all tab characters.
1445
+ * @returns {string}
1446
+ * A new version of `text` with all of the minimally indented lines having
1447
+ * no leading spacing and all other indented lines following suit. If
1448
+ * `opt_options.trim` is `true` all leading lines and trailing spaces will
1449
+ * not exist. All tab characters will be replaced with
1450
+ * `opt_options.tabSize` amount of space characters.
1451
+ */
1452
+ function unindentMin(text, opt_options) {
1453
+ opt_options = Object(opt_options);
1454
+ const tabSize = opt_options.tabSize ?? 4;
1455
+ const trim = opt_options.trim ?? true;
1456
+ text = text.replace(/\t/g, ' '.repeat(tabSize));
1457
+ if (!/(^|[\r\n])\S/.test(text)) {
1458
+ const rgx = /(^|[\r\n])((?:(?!\r|\n)\s)+)(?=(\S)?)/g;
1459
+ let min = Infinity;
1460
+ for (let match; match = rgx.exec(text);) {
1461
+ if (match[3]) {
1462
+ min = Math.min(min, match[2].length);
1463
+ }
1464
+ }
1465
+ text = text.replace(
1466
+ rgx,
1467
+ (_, start, spaces) => start + spaces.slice(min)
1468
+ );
1469
+ }
1470
+ return trim ? text.replace(/^(\s*[\r\n]+)+|\s+$/g, '') : text;
1471
+ }
1472
+
1473
+ /**
1474
+ * Picks the properties of a description sent by the runner so that nothing
1475
+ * unexpected makes its way into the displays.
1476
+ * @param {*} description
1477
+ */
1478
+ function sanitizeDescription(description) {
1479
+ description = Object(description);
1480
+ return {
1481
+ typeName: `${description.typeName}`,
1482
+ isPrimitive: !!description.isPrimitive,
1483
+ // Each part is [kind, text] where kind is used in a class name.
1484
+ parts: Array.from(description.parts ?? [], part => [
1485
+ /^[a-z]+$/.test(Object(part)[0]) ? part[0] : 'text',
1486
+ `${Object(part)[1]}`,
1487
+ ]),
1488
+ ...sanitizeEntries(description),
1489
+ };
1490
+ }
1491
+
1492
+ /**
1493
+ * Picks the entries of a description sent by the runner.
1494
+ * @param {*} description
1495
+ */
1496
+ function sanitizeEntries(description) {
1497
+ description = Object(description);
1498
+ const result = {};
1499
+ for (const key of ['entries', 'protoEntries']) {
1500
+ if (Array.isArray(description[key])) {
1501
+ // Each entry is [key, description, isEnumerable].
1502
+ result[key] = description[key].map(entry => [
1503
+ `${Object(entry)[0]}`,
1504
+ sanitizeDescription(Object(entry)[1]),
1505
+ Object(entry)[2] !== false,
1506
+ ]);
1507
+ }
1508
+ }
1509
+ return result;
1510
+ }
1511
+
1512
+ /**
1513
+ * NOTE: Called via main by the runner.
1514
+ * @param {object} options
1515
+ * @param {string} options.logId
1516
+ * @param {string} options.key
1517
+ * @param {any[]} options.descriptions
1518
+ * @param {{headers: string[], rows: {index: string, cells: any[]}[]}|null} options.table
1519
+ */
1520
+ function appendLog({logId, key, descriptions, table}) {
1521
+ key = `${key}`;
1522
+ mountedApp.displays.push({
1523
+ type: 'log',
1524
+ key,
1525
+ name: `console.${key}`,
1526
+ descriptions: Array.from(descriptions, sanitizeDescription),
1527
+ table: table ? {
1528
+ headers: Array.from(table.headers, h => `${h}`),
1529
+ rows: Array.from(table.rows, row => ({
1530
+ index: `${row.index}`,
1531
+ cells: Array.from(row.cells, cell => cell && sanitizeDescription(cell)),
1532
+ })),
1533
+ } : null,
1534
+ logId: `${logId}`,
1535
+ });
1536
+ }
1537
+
1538
+ /**
1539
+ * NOTE: Called via main by the runner.
1540
+ * @param {object} options
1541
+ * @param {string} options.message
1542
+ * @param {number=} options.line
1543
+ * @param {number=} options.column
1544
+ */
1545
+ function appendError({message, line, column}) {
1546
+ mountedApp.displays.push({
1547
+ type: 'error',
1548
+ message: `${message}`,
1549
+ line: +line,
1550
+ column: +column,
1551
+ });
1552
+ }
1553
+
1554
+ /**
1555
+ * NOTE: Called via main by the runner.
1556
+ * @param {(string|number)[]} path
1557
+ * The log ID, the argument index and then pairs of entry group keys and entry
1558
+ * indices.
1559
+ * @param {*} description
1560
+ */
1561
+ function updateDescriptionFor(path, description) {
1562
+ path = Array.from(path);
1563
+ const logId = path.shift();
1564
+ const display = mountedApp.displays.find(d => d.type === 'log' && d.logId === logId);
1565
+ let level = display?.descriptions[toIndex(path.shift())];
1566
+
1567
+ for (let i = 0; level && i < path.length; i += 2) {
1568
+ const groupKey = path[i];
1569
+ if (groupKey !== 'entries' && groupKey !== 'protoEntries') return;
1570
+ level = level[groupKey]?.[toIndex(path[i + 1])]?.[1];
1571
+ }
1572
+
1573
+ if (level) Object.assign(level, sanitizeEntries(description));
1574
+ }
1575
+
1576
+ /**
1577
+ * NOTE: Called via main by the runner once it has run a group of code.
1578
+ */
1579
+ function onCodeRan() {
1580
+ mountedApp.runningCount = Math.max(0, mountedApp.runningCount - 1);
1581
+ mountedApp.runHiddenGroups();
1582
+ }
1583
+
1584
+ /**
1585
+ * NOTE: Called via main by the runner when console.clear() is called.
1586
+ */
1587
+ function clearDisplays() {
1588
+ mountedApp.displays = [{type: 'notice', message: 'Console was cleared'}];
1589
+ }
1590
+
1591
+ /**
1592
+ * Only allows non-negative integers to be used as an index.
1593
+ * @param {*} value
1594
+ * @returns {number|undefined}
1595
+ */
1596
+ function toIndex(value) {
1597
+ return Number.isInteger(value) && value >= 0 ? value : undefined;
1598
+ }
1599
+
1600
+ },
1601
+ jsUrls: [
1602
+ 'https://unpkg.com/vue@3/dist/vue.global.prod.js',
1603
+ 'https://unpkg.com/ace-builds@1/src-noconflict/ace.js',
1604
+ 'https://unpkg.com/prismjs@1/components/prism-core.min.js',
1605
+ 'https://unpkg.com/prismjs@1/plugins/autoloader/prism-autoloader.min.js',
1606
+ 'https://unpkg.com/prismjs@1/plugins/match-braces/prism-match-braces.min.js',
1607
+ ],
1608
+ cssUrls: [
1609
+ 'data:text/css,' + encodeURIComponent(VIEWER_IFRAME_CSS),
1610
+ 'https://unpkg.com/prism-themes@1/themes/prism-vsc-dark-plus.min.css',
1611
+ 'https://unpkg.com/prismjs@1/plugins/match-braces/prism-match-braces.min.css',
1612
+ ],
1613
+ htmlAttributes: 'data-theme="' + theme + '"',
1614
+ onMessage(message) {
1615
+ relayMessage(message.data, {runner});
1616
+ },
1617
+ async onReady() {
1618
+ this.call('init', script.textContent, dataset);
1619
+ },
1620
+ body: VIEWER_IFRAME_HTML,
1621
+ style: {
1622
+ width: '100%',
1623
+ height: '100%',
1624
+ border: 0,
1625
+ display: 'block',
1626
+ },
1627
+ });
1628
+
1629
+ script.parentNode.insertBefore(callableViewerFrame.iframe, script);
1630
+ }
1631
+
1632
+ // NOTE: This solution was intentionally written without using newer JS
1633
+ // features to make the minified version even smaller.
1634
+ var createCallableFrame = (function () {
1635
+ var IFRAME_SCRIPT_MESSAGE_CODE = parseFunction(function() {
1636
+ // NOTE: Referencing with window to ensure that local namespace will not
1637
+ // interfere.
1638
+ window.addEventListener('message', function(e) {
1639
+ var data = e.data;
1640
+ if (
1641
+ e.source === window.parent
1642
+ && data && /^[A-Za-z_$][\w$]*$/.test(data.funcName)
1643
+ && Array.isArray(data.args)
1644
+ ) {
1645
+ var func = eval(data.funcName);
1646
+ if ('function' === typeof func) func.apply(e, data.args);
1647
+ }
1648
+ });
1649
+
1650
+ function messageParent(message) {
1651
+ window.parent.postMessage(message, '*');
1652
+ }
1653
+ }).body;
1654
+
1655
+ /**
1656
+ * Creates an IFRAME which allows for easier 2-way communication.
1657
+ * @template {createCallableFrame_Return} R
1658
+ * @param {Object} options
1659
+ * @param {(string|Function)=} options.jsCode
1660
+ * The JavaScript code that will be encapsulated and run within the IFRAME.
1661
+ * The code can call `messageParent(message)` to send a message to
1662
+ * `options.onMessage()`. The code can call `callParent(funcName, ...args)`
1663
+ * to execute `options.functions[funcName](...args)`. The code can call
1664
+ * `applyParent(funcName, args)` to execute
1665
+ * `options.functions[funcName](...args)`.
1666
+ * @param {{[funcName: string]: (this: R, ...args) => void}=} options.functions
1667
+ * Functions that can be called by the IFRAME code to execute code on the
1668
+ * parent level.
1669
+ * @param {string[]=} options.jsUrls
1670
+ * @param {string[]=} options.cssUrls
1671
+ * @param {string=} options.head
1672
+ * @param {string=} options.body
1673
+ * @param {(CSSStyleDeclaration|string)=} options.style
1674
+ * The HTML code that will be used to instantiate the page.
1675
+ * @param {string=} options.htmlAttributes
1676
+ * Attributes to add to the `<html>` element of the IFRAME.
1677
+ * @param {((this: R, event: MessageEvent) => void)=} options.onMessage
1678
+ * @param {((this: R, event: MessageEvent) => void)=} options.onReady
1679
+ * @returns {R}
1680
+ */
1681
+ function createCallableFrame(options) {
1682
+ // Break some of the options out into their own variables.
1683
+ var jsCode = options.jsCode;
1684
+ var onMessage = options.onMessage;
1685
+ var onReady = options.onReady;
1686
+ var style = options.style;
1687
+
1688
+ // isReady indicates if the IFRAME is ready to have messages sent to it
1689
+ // while READY_ID is used internally to confirm if the IFRAME is actually
1690
+ // ready to receive function calls.
1691
+ var isReady, READY_ID = Math.random() + '' + Math.random();
1692
+
1693
+ // The script code for the IFRAME.
1694
+ var IFRAME_SCRIPT_CODE = [
1695
+ '(function(){',
1696
+ IFRAME_SCRIPT_MESSAGE_CODE,
1697
+ 'var applyParent, callParent;',
1698
+ '(function(READY_ID){',
1699
+ parseFunction(function() {
1700
+ applyParent = function(funcName, args) {
1701
+ window.parent.postMessage({funcName: funcName, args: args, id: READY_ID}, '*');
1702
+ };
1703
+
1704
+ callParent = function(funcName) {
1705
+ window.parent.postMessage(
1706
+ {funcName: funcName, args: Array.prototype.slice.call(arguments, 1), id: READY_ID},
1707
+ '*'
1708
+ );
1709
+ };
1710
+
1711
+ var interval = setInterval(function() {
1712
+ if (/^(complete|interactive)$/.test(document.readyState)) {
1713
+ clearInterval(interval);
1714
+ messageParent(READY_ID);
1715
+ }
1716
+ }, 100);
1717
+ }).body,
1718
+ '})(' + JSON.stringify(READY_ID) + ');',
1719
+ 'function' !== typeof jsCode ? jsCode || '' : parseFunction(jsCode).body,
1720
+ '})();',
1721
+ ].join('\n')
1722
+ // Prevents the HTML parser from ending the inline script early.
1723
+ .replace(/<(?=\/script|!--)/gi, '\\x3C');
1724
+
1725
+ // Creates the IFRAME and sets its source via srcdoc.
1726
+ var IFRAME = document.createElement('iframe');
1727
+ var HTML_CODE = [
1728
+ '<!DOCTYPE html>',
1729
+ '<html ' + (options.htmlAttributes || '') + '>',
1730
+ '<head>',
1731
+ options.head || '',
1732
+ (options.cssUrls || []).map(function(cssUrl) {
1733
+ return '<link href="' + cssUrl + '" rel="stylesheet">';
1734
+ }).join('\n'),
1735
+ '</head>',
1736
+ '<body>',
1737
+ options.body || '',
1738
+ // The scripts are loaded after the body so that the body can be shown
1739
+ // while they load.
1740
+ (options.jsUrls || []).map(function(jsUrl) {
1741
+ return '<script src="' + jsUrl + '"><\x2fscript>';
1742
+ }).join('\n'),
1743
+ '<script>' + IFRAME_SCRIPT_CODE + '<\x2fscript>',
1744
+ '</body>',
1745
+ '</html>'
1746
+ ].join('\n');
1747
+ IFRAME.srcdoc = HTML_CODE;
1748
+
1749
+ // Set the style of the iframe.
1750
+ if (style) {
1751
+ if ('string' === typeof style) {
1752
+ IFRAME.style.cssText = style;
1753
+ }
1754
+ else {
1755
+ for (var styleKey in style) {
1756
+ if (hasOwn(style, styleKey)) {
1757
+ IFRAME.style[styleKey] = style[styleKey];
1758
+ }
1759
+ }
1760
+ }
1761
+ }
1762
+
1763
+ // Adds an event listener so that messages from the IFRAME will be captured.
1764
+ addEventListener('message', function(e) {
1765
+ if (e.source === IFRAME.contentWindow) {
1766
+ var data = e.data;
1767
+ var dataIsReadyId = READY_ID === data;
1768
+ if (isReady && !dataIsReadyId) {
1769
+ if (options.functions && data.id === READY_ID && hasOwn(options.functions, data.funcName)) {
1770
+ options.functions[data.funcName].apply(callableFrame, data.args);
1771
+ }
1772
+ else if ('function' === typeof onMessage) {
1773
+ onMessage.call(callableFrame, e);
1774
+ }
1775
+ else {
1776
+ console.warn('Message sent from callable frame but no listener was set up.', e);
1777
+ }
1778
+ }
1779
+ else if (dataIsReadyId) {
1780
+ isReady = true;
1781
+ if ('function' === typeof onReady) onReady.call(callableFrame, e);
1782
+ // Sends anything that was queued before the IFRAME was ready.
1783
+ while (queuedMessages.length) postToFrame(queuedMessages.shift());
1784
+ }
1785
+ else { // !isReady
1786
+ console.warn('Message sent from callable frame prematurely:', e);
1787
+ }
1788
+ }
1789
+ });
1790
+
1791
+ // Returns an object which makes it possible to call functions and get
1792
+ // access to the IFRAME.
1793
+ var queuedMessages = [];
1794
+ function postToFrame(message) {
1795
+ if (isReady) IFRAME.contentWindow.postMessage(message, '*');
1796
+ else queuedMessages.push(message);
1797
+ }
1798
+ var callableFrame = {
1799
+ apply: function(funcName, args) {
1800
+ postToFrame({funcName: funcName, args: args});
1801
+ },
1802
+ call: function(funcName) {
1803
+ postToFrame({funcName: funcName, args: Array.prototype.slice.call(arguments, 1)});
1804
+ },
1805
+ iframe: IFRAME
1806
+ };
1807
+ return callableFrame;
1808
+ };
1809
+ /**
1810
+ * @typedef {Object} createCallableFrame_Return
1811
+ * @property {(funcName: string, args: any[]) => void} apply
1812
+ * @property {(funcName: string, ...args: any[]) => void} call
1813
+ * @property {HTMLIFrameElement} iframe
1814
+ */
1815
+
1816
+ /**
1817
+ * Determines if `obj` has its own property named `prop`.
1818
+ * @type {(obj: any, prop: string|symbol) => boolean}
1819
+ */
1820
+ var hasOwn = atob.call.bind({}.hasOwnProperty);
1821
+
1822
+ /**
1823
+ * Parses a user-defined function to determine if it is an arrow function, an
1824
+ * async function a generator function and also gets the parameters as a string
1825
+ * and the function body as a string.
1826
+ * @param {Function} input
1827
+ * @returns {{
1828
+ * isArrow: boolean;
1829
+ * isAsync: boolean;
1830
+ * isGenerator: boolean;
1831
+ * parameters: string;
1832
+ * body: string;
1833
+ * }}
1834
+ */
1835
+ function parseFunction(input) {
1836
+ // Get rid of all comments encountered before the function body.
1837
+ var strFunc = input + '';
1838
+ /** @type {RegExpExecArray?} */
1839
+ var m;
1840
+ while (m = /\/\*[^]*?\*\/|\/\/.*/g.exec(strFunc)) {
1841
+ var STR_BEFORE = strFunc.slice(0, m.index);
1842
+ if (/[\{"'`]/.test(STR_BEFORE)) break;
1843
+ strFunc = STR_BEFORE + ' ' + strFunc.slice(m.index + m[0].length);
1844
+ }
1845
+
1846
+ // Determines if the function is an async function.
1847
+ var IS_ASYNC = /^\s*async\b/.test(strFunc);
1848
+ if (IS_ASYNC) strFunc = strFunc.replace('async', '');
1849
+
1850
+ // Determines if the function is a generator function.
1851
+ var IS_GENERATOR = /^\s*(?:function\s*)?\*/.test(strFunc);
1852
+ if (IS_GENERATOR) strFunc = strFunc.replace(/(?:\bfunction\s*)?\*/, '');
1853
+
1854
+ // Determines if the function is an arrow function.
1855
+ var ARROW_MATCH = /\s*=>\s*/.exec(strFunc);
1856
+ var INDEX_OF_FUNC_START = strFunc.search(/\)\s*\{/);
1857
+ var IS_ARROW = ARROW_MATCH && (ARROW_MATCH.index < INDEX_OF_FUNC_START || INDEX_OF_FUNC_START < 0);
1858
+
1859
+ // Parses out the parameters and body as strings.
1860
+ var parameters, body;
1861
+ if (IS_ARROW) {
1862
+ var STR_BEFORE = strFunc.slice(0, ARROW_MATCH.index);
1863
+ var STR_AFTER = strFunc.slice(ARROW_MATCH.index + ARROW_MATCH[0].length);
1864
+ parameters = (/\S+(?:\s*,\s*\S+)*/.exec(STR_BEFORE.replace(/[()]/g, ' ')) ?? [])[0];
1865
+ body = !/^\{/.test(STR_AFTER)
1866
+ ? 'return ' + STR_AFTER
1867
+ : STR_AFTER.replace(/^\{|\}\s*$/g, '');
1868
+ }
1869
+ else {
1870
+ parameters = (/\(([^\)]*)\)/.exec(strFunc) ?? [])[1];
1871
+ body = (/\{([^]*)\}/.exec(strFunc) ?? [])[1];
1872
+ }
1873
+
1874
+ // Returns the parsed function.
1875
+ return {
1876
+ isAsync: IS_ASYNC,
1877
+ isGenerator: IS_GENERATOR,
1878
+ isArrow: IS_ARROW,
1879
+ parameters,
1880
+ body,
1881
+ };
1882
+ }
1883
+
1884
+ // Make parseFunction() available.
1885
+ createCallableFrame.parseFunction = parseFunction;
1886
+
1887
+ return createCallableFrame;
1888
+ })();
1889
+
1890
+ main(document.currentScript);
1891
+ })();