This is a visualization of JavaScript/CSS source map data, which is useful for debugging problems with
- generated source maps. It's designed to be high-performance so it doesn't fall over with huge source maps.
-
Drag and drop some files here or to get started. You can either
- drop a single JavaScript/CSS file with an inline source map comment, or a JavaScript/CSS file and a separate
- source map file together.
-
Or you can to play around with the visualization.
-
-
-
-
-
-
-
Original code
-
-
-
-
Generated code
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
Source Map Visualization
+
+ This is a visualization of JavaScript/CSS source map data,
+ which is useful for debugging problems with generated source maps.
+ It's designed to be high-performance so it doesn't fall over with huge
+ source maps.
+
+
+ Drag and drop some files here or
+ to get started. You can
+ either drop a single JavaScript/CSS file with an inline source
+ map comment, or a JavaScript/CSS file and a separate source map
+ file together.
+
+
+ Or you can to play
+ around with the visualization.
+
+
+
+
+
+
+
+
Original code
+
+
+
+
Generated code
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..b8eadee
--- /dev/null
+++ b/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "source-map-visualization",
+ "private": true,
+ "version": "1.0.0",
+ "scripts": {
+ "dev": "rsbuild dev --open",
+ "build": "rsbuild build",
+ "preview": "rsbuild preview"
+ },
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "source-map": "^0.7.4",
+ "type-fest": "^4.21.0"
+ },
+ "devDependencies": {
+ "@rsbuild/core": "1.0.0-alpha.5",
+ "@rsbuild/plugin-react": "1.0.0-alpha.5",
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "typescript": "^5.5.3"
+ }
+}
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
new file mode 100644
index 0000000..3637918
--- /dev/null
+++ b/pnpm-lock.yaml
@@ -0,0 +1,360 @@
+lockfileVersion: '6.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+dependencies:
+ react:
+ specifier: ^18.3.1
+ version: 18.3.1
+ react-dom:
+ specifier: ^18.3.1
+ version: 18.3.1(react@18.3.1)
+ source-map:
+ specifier: ^0.7.4
+ version: 0.7.4
+ type-fest:
+ specifier: ^4.21.0
+ version: 4.21.0
+
+devDependencies:
+ '@rsbuild/core':
+ specifier: 1.0.0-alpha.5
+ version: 1.0.0-alpha.5
+ '@rsbuild/plugin-react':
+ specifier: 1.0.0-alpha.5
+ version: 1.0.0-alpha.5(@rsbuild/core@1.0.0-alpha.5)
+ '@types/react':
+ specifier: ^18.3.3
+ version: 18.3.3
+ '@types/react-dom':
+ specifier: ^18.3.0
+ version: 18.3.0
+ typescript:
+ specifier: ^5.5.3
+ version: 5.5.3
+
+packages:
+
+ /@module-federation/runtime-tools@0.2.3:
+ resolution: {integrity: sha512-capN8CVTCEqNAjnl102girrkevczoQfnQYyiYC4WuyKsg7+LUqfirIe1Eiyv6VSE2UgvOTZDnqvervA6rBOlmg==}
+ dependencies:
+ '@module-federation/runtime': 0.2.3
+ '@module-federation/webpack-bundler-runtime': 0.2.3
+ dev: true
+
+ /@module-federation/runtime@0.2.3:
+ resolution: {integrity: sha512-N+ZxBUb1mkmfO9XT1BwgYQgShtUTlijHbukqQ4afFka5lRAT+ayC7RKfHJLz0HbuexKPCmPBDfdmCnErR5WyTQ==}
+ dependencies:
+ '@module-federation/sdk': 0.2.3
+ dev: true
+
+ /@module-federation/sdk@0.2.3:
+ resolution: {integrity: sha512-W9zrPchLocyCBc/B8CW21akcfJXLl++9xBe1L1EtgxZGfj/xwHt0GcBWE/y+QGvYTL2a1iZjwscbftbUhxgxXg==}
+ dev: true
+
+ /@module-federation/webpack-bundler-runtime@0.2.3:
+ resolution: {integrity: sha512-L/jt2uJ+8dwYiyn9GxryzDR6tr/Wk8rpgvelM2EBeLIhu7YxCHSmSjQYhw3BTux9zZIr47d1K9fGjBFsVRd/SQ==}
+ dependencies:
+ '@module-federation/runtime': 0.2.3
+ '@module-federation/sdk': 0.2.3
+ dev: true
+
+ /@rsbuild/core@1.0.0-alpha.5:
+ resolution: {integrity: sha512-zcEwgzeuDHUjSpBBRMfk6+seYLZKLTFa89wdQuSSgylCfvAahfz/Pea5PoblzfSd1PP8Z/5h+Z0Er2WYS+GMWg==}
+ engines: {node: '>=16.7.0'}
+ hasBin: true
+ dependencies:
+ '@rspack/core': 1.0.0-alpha.1(@swc/helpers@0.5.11)
+ '@swc/helpers': 0.5.11
+ caniuse-lite: 1.0.30001640
+ core-js: 3.37.1
+ html-rspack-plugin: 5.8.0(@rspack/core@1.0.0-alpha.1)
+ postcss: 8.4.39
+ optionalDependencies:
+ fsevents: 2.3.3
+ dev: true
+
+ /@rsbuild/plugin-react@1.0.0-alpha.5(@rsbuild/core@1.0.0-alpha.5):
+ resolution: {integrity: sha512-zdt6MzFgTknsybQohSFWq+274O6mik0tCO9rKDJw8/tme3jNpOLxZXBoBMdoGKd+vm1z/SoWvHEtB37+U6MpVQ==}
+ peerDependencies:
+ '@rsbuild/core': ^1.0.0-alpha.5
+ dependencies:
+ '@rsbuild/core': 1.0.0-alpha.5
+ '@rspack/plugin-react-refresh': 1.0.0-alpha.1(react-refresh@0.14.2)
+ react-refresh: 0.14.2
+ dev: true
+
+ /@rspack/binding-darwin-arm64@1.0.0-alpha.1:
+ resolution: {integrity: sha512-6ZbxlS5+29bvYqsEoPMvCnn6NxZPq8CucuDDSwuP/w0UH81DD2yf9Mtn7IYDBhjiS7wv9pF4NYr7oDpGfOksHA==}
+ cpu: [arm64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rspack/binding-darwin-x64@1.0.0-alpha.1:
+ resolution: {integrity: sha512-4Y9xfwaTHJBTrq5uav/tOWvCMLcBG72SEi+XgVjsZOC2ZS2WNqg4z0QCRzmt+agdmlWuXTOMB2LpYxRLxBj9sg==}
+ cpu: [x64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rspack/binding-linux-arm64-gnu@1.0.0-alpha.1:
+ resolution: {integrity: sha512-cew/WxOILZkTN1I+oWrt1BSnm3iK4/8DqX/6XxiUwCAmgfStZXB5NQS+SDIDBzFJ4I+NibLRMiJy5T9uCtOWzQ==}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rspack/binding-linux-arm64-musl@1.0.0-alpha.1:
+ resolution: {integrity: sha512-+yLjjl8nkWRseQwwovaaLMshTKTep/5PSFN3nHtXPo/TsL1itDgUtM9XntTdeTdaIEgVyNGcb1dRARDoAq/vKw==}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rspack/binding-linux-x64-gnu@1.0.0-alpha.1:
+ resolution: {integrity: sha512-QQfGCTrn76d6fVoWnn6tm2eYTSJe78wc3X4H92Js+ZhjcHVn5XSTeyqyb0Oq4+TRKmwAi39/wUgPsbSxQj9g5A==}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rspack/binding-linux-x64-musl@1.0.0-alpha.1:
+ resolution: {integrity: sha512-ZpVTXPjG5SLwKUYiwLR6XIlo/G4ZQHA6TBKFbPG3p9kZ0DYurGjt8bcjeiu9xP050XmTA0Hj5vS6zEdYtgE6bA==}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rspack/binding-win32-arm64-msvc@1.0.0-alpha.1:
+ resolution: {integrity: sha512-8wTNt1MgJgUMRJ2ySAToUZoeBJabOV/lqH2o1ypHdmirZ0aMno+C05XOmbrLhgZL2qKBROVt4VVa7+IlmvJ7yQ==}
+ cpu: [arm64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rspack/binding-win32-ia32-msvc@1.0.0-alpha.1:
+ resolution: {integrity: sha512-hfujEyp1+0yfAzShQJZUezdiHQ7KvTwC0T817/dQi4CdwoXWOuPnnYrb5JG5TAcagNttQNX2NFhVbeYQgj5c8Q==}
+ cpu: [ia32]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rspack/binding-win32-x64-msvc@1.0.0-alpha.1:
+ resolution: {integrity: sha512-KMIosN4wdXVFb3RIBX7fEXBv5jfinK0PkYSv8aQcIAdyhm2mSI95aCpAFE8xhgGCeSwaZ2PCO0zBgO0ZldAc7Q==}
+ cpu: [x64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rspack/binding@1.0.0-alpha.1:
+ resolution: {integrity: sha512-3q3cN5kZZdaAnIrjVhkW2f2RbLpxgSp8ATs4P6fzUoKvuunU1v+KXbhPir/tKaKBXLgYH2h3i13tPDcuL3kA+A==}
+ optionalDependencies:
+ '@rspack/binding-darwin-arm64': 1.0.0-alpha.1
+ '@rspack/binding-darwin-x64': 1.0.0-alpha.1
+ '@rspack/binding-linux-arm64-gnu': 1.0.0-alpha.1
+ '@rspack/binding-linux-arm64-musl': 1.0.0-alpha.1
+ '@rspack/binding-linux-x64-gnu': 1.0.0-alpha.1
+ '@rspack/binding-linux-x64-musl': 1.0.0-alpha.1
+ '@rspack/binding-win32-arm64-msvc': 1.0.0-alpha.1
+ '@rspack/binding-win32-ia32-msvc': 1.0.0-alpha.1
+ '@rspack/binding-win32-x64-msvc': 1.0.0-alpha.1
+ dev: true
+
+ /@rspack/core@1.0.0-alpha.1(@swc/helpers@0.5.11):
+ resolution: {integrity: sha512-UN6oAWnDJpouldf6UDuZZIc1GSgEgSAeeIQlpCwob9v+uuZ/NjJHvG7HCjxeHtkh1g9Oly8clOZA7gKOWtE4CA==}
+ engines: {node: '>=16.0.0'}
+ peerDependencies:
+ '@swc/helpers': '>=0.5.1'
+ peerDependenciesMeta:
+ '@swc/helpers':
+ optional: true
+ dependencies:
+ '@module-federation/runtime-tools': 0.2.3
+ '@rspack/binding': 1.0.0-alpha.1
+ '@rspack/lite-tapable': 1.0.0-alpha.1
+ '@swc/helpers': 0.5.11
+ caniuse-lite: 1.0.30001640
+ dev: true
+
+ /@rspack/lite-tapable@1.0.0-alpha.1:
+ resolution: {integrity: sha512-vgY3jauZk+Pd6u6I5d4Rs9Q7hUtl5mClKG2Hn/FucFL4WK+1m6kssu/576WEY6HP5ptBPWlqcBbamzodai1q1g==}
+ engines: {node: '>=16.0.0'}
+ dev: true
+
+ /@rspack/plugin-react-refresh@1.0.0-alpha.1(react-refresh@0.14.2):
+ resolution: {integrity: sha512-lIQBT3xbaf8wStCt8gRI2LUoYTYix3Z4GYqQGjVGX66ypGliIR5N7yfs318nbjzD7mr5KJmv6H4XG+AtXrElYQ==}
+ peerDependencies:
+ react-refresh: '>=0.10.0 <1.0.0'
+ peerDependenciesMeta:
+ react-refresh:
+ optional: true
+ dependencies:
+ error-stack-parser: 2.1.4
+ html-entities: 2.5.2
+ react-refresh: 0.14.2
+ dev: true
+
+ /@swc/helpers@0.5.11:
+ resolution: {integrity: sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==}
+ dependencies:
+ tslib: 2.6.3
+ dev: true
+
+ /@types/prop-types@15.7.12:
+ resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==}
+ dev: true
+
+ /@types/react-dom@18.3.0:
+ resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==}
+ dependencies:
+ '@types/react': 18.3.3
+ dev: true
+
+ /@types/react@18.3.3:
+ resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==}
+ dependencies:
+ '@types/prop-types': 15.7.12
+ csstype: 3.1.3
+ dev: true
+
+ /caniuse-lite@1.0.30001640:
+ resolution: {integrity: sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==}
+ dev: true
+
+ /core-js@3.37.1:
+ resolution: {integrity: sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==}
+ requiresBuild: true
+ dev: true
+
+ /csstype@3.1.3:
+ resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+ dev: true
+
+ /error-stack-parser@2.1.4:
+ resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==}
+ dependencies:
+ stackframe: 1.3.4
+ dev: true
+
+ /fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /html-entities@2.5.2:
+ resolution: {integrity: sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==}
+ dev: true
+
+ /html-rspack-plugin@5.8.0(@rspack/core@1.0.0-alpha.1):
+ resolution: {integrity: sha512-ilfK60cxmBzglkHw91SlHDwbTd8uS7+poG12ueuwn012XPdFq8jU0pFuGEqoryJ+1l/uQuVffJ2jlpJDlhJBsg==}
+ engines: {node: '>=10.13.0'}
+ peerDependencies:
+ '@rspack/core': 0.x || 1.x
+ peerDependenciesMeta:
+ '@rspack/core':
+ optional: true
+ dependencies:
+ '@rspack/core': 1.0.0-alpha.1(@swc/helpers@0.5.11)
+ dev: true
+
+ /js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+ dev: false
+
+ /loose-envify@1.4.0:
+ resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
+ hasBin: true
+ dependencies:
+ js-tokens: 4.0.0
+ dev: false
+
+ /nanoid@3.3.7:
+ resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+ dev: true
+
+ /picocolors@1.0.1:
+ resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
+ dev: true
+
+ /postcss@8.4.39:
+ resolution: {integrity: sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==}
+ engines: {node: ^10 || ^12 || >=14}
+ dependencies:
+ nanoid: 3.3.7
+ picocolors: 1.0.1
+ source-map-js: 1.2.0
+ dev: true
+
+ /react-dom@18.3.1(react@18.3.1):
+ resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
+ peerDependencies:
+ react: ^18.3.1
+ dependencies:
+ loose-envify: 1.4.0
+ react: 18.3.1
+ scheduler: 0.23.2
+ dev: false
+
+ /react-refresh@0.14.2:
+ resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /react@18.3.1:
+ resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ loose-envify: 1.4.0
+ dev: false
+
+ /scheduler@0.23.2:
+ resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
+ dependencies:
+ loose-envify: 1.4.0
+ dev: false
+
+ /source-map-js@1.2.0:
+ resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /source-map@0.7.4:
+ resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==}
+ engines: {node: '>= 8'}
+ dev: false
+
+ /stackframe@1.3.4:
+ resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==}
+ dev: true
+
+ /tslib@2.6.3:
+ resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==}
+ dev: true
+
+ /type-fest@4.21.0:
+ resolution: {integrity: sha512-ADn2w7hVPcK6w1I0uWnM//y1rLXZhzB9mr0a3OirzclKF1Wp6VzevUmzz/NRAWunOT6E8HrnpGY7xOfc6K57fA==}
+ engines: {node: '>=16'}
+ dev: false
+
+ /typescript@5.5.3:
+ resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+ dev: true
diff --git a/rsbuild.config.ts b/rsbuild.config.ts
new file mode 100644
index 0000000..04c3f82
--- /dev/null
+++ b/rsbuild.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from '@rsbuild/core';
+
+export default defineConfig({
+ html: {
+ template: './index.html',
+ },
+});
diff --git a/src/env.d.ts b/src/env.d.ts
new file mode 100644
index 0000000..b0ac762
--- /dev/null
+++ b/src/env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..6becb9d
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,2802 @@
+import './style.css';
+import { Promisable } from 'type-fest';
+import { $, $assert, assert } from './utils';
+import { RawIndexMap, RawSourceMap } from 'source-map';
+
+////////////////////////////////////////////////////////////////////////////////
+// Dragging
+
+const dragTarget = $('#dragTarget');
+const uploadFiles = $('#uploadFiles');
+const loadExample = $('#loadExample');
+
+let dragging = 0;
+let filesInput: HTMLInputElement;
+
+function isFilesDragEvent(e: DragEvent) {
+ return (
+ e.dataTransfer &&
+ e.dataTransfer.types &&
+ Array.prototype.indexOf.call(e.dataTransfer.types, 'Files') !== -1
+ );
+}
+
+document.ondragover = e => {
+ e.preventDefault();
+};
+
+document.ondragenter = e => {
+ e.preventDefault();
+ if (!isFilesDragEvent(e)) return;
+ dragTarget.style.display = 'block';
+ dragging++;
+};
+
+document.ondragleave = e => {
+ e.preventDefault();
+ if (!isFilesDragEvent(e)) return;
+ if (--dragging === 0) dragTarget.style.display = 'none';
+};
+
+document.ondrop = e => {
+ e.preventDefault();
+ dragTarget.style.display = 'none';
+ dragging = 0;
+ if (e.dataTransfer && e.dataTransfer.files)
+ startLoading(e.dataTransfer.files);
+};
+
+uploadFiles.onclick = () => {
+ if (filesInput) document.body.removeChild(filesInput);
+ filesInput = document.createElement('input');
+ filesInput.type = 'file';
+ filesInput.multiple = true;
+ filesInput.style.display = 'none';
+ document.body.appendChild(filesInput);
+ filesInput.click();
+ filesInput.onchange = () =>
+ filesInput.files && startLoading(filesInput.files);
+};
+
+loadExample.onclick = () => {
+ finishLoading(exampleJS, exampleMap);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// Loading
+
+const utf8ToUTF16 = (x: string) => decodeURIComponent(escape(x));
+const utf16ToUTF8 = (x: string) => unescape(encodeURIComponent(x));
+
+const promptText = $('#promptText');
+const errorText = $('#errorText');
+const toolbar = $('#toolbar');
+const statusBar = $('#statusBar');
+const progressBarOverlay = $('#progressBar');
+const progressBar = $('#progressBar .progress');
+const originalStatus = $('#originalStatus');
+const generatedStatus = $('#generatedStatus');
+const fileList = $('select#fileList');
+
+function isProbablySourceMap(file: File) {
+ return file.name.endsWith('.map') || file.name.endsWith('.json');
+}
+
+function loadFile(file: File) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onerror = reject;
+ reader.onloadend = () => resolve(reader.result && reader.result.toString());
+ reader.readAsText(file);
+ });
+}
+
+function resetLoadingState() {
+ promptText.style.display = 'block';
+ toolbar.style.display = 'none';
+ statusBar.style.display = 'none';
+ canvas.style.display = 'none';
+}
+
+function showLoadingError(text: string) {
+ resetLoadingState();
+ errorText.style.display = 'block';
+ errorText.textContent = text;
+
+ // Push an empty hash since the state has been cleared
+ if (location.hash !== '') {
+ try {
+ history.pushState({}, '', location.pathname);
+ } catch (e) { }
+ }
+}
+
+async function finishLoadingCodeWithEmbeddedSourceMap(
+ code: string,
+ file: File | null,
+) {
+ let url = '';
+ let match: RegExpExecArray | null;
+
+ // Check for both "//" and "/*" comments. This is mostly done manually
+ // instead of doing it all with a regular expression because Firefox's
+ // regular expression engine crashes with an internal error when the
+ // match is too big.
+ for (
+ let regex = /\/([*/])[#@] *sourceMappingURL=/g;
+ (match = regex.exec(code));
+
+ ) {
+ const start = match.index + match[0].length;
+ const n = code.length;
+ let end = start;
+ while (end < n && code.charCodeAt(end) > 32) {
+ end++;
+ }
+ if (
+ end > start &&
+ (match[1] === '/' || code.slice(end).indexOf('*/') > 0)
+ ) {
+ url = code.slice(start, end);
+ break;
+ }
+ }
+
+ // Check for a non-empty data URL payload
+ if (url) {
+ let map;
+ try {
+ // Use "new URL" to ensure that the URL has a protocol (e.g. "data:" or "https:")
+ map = await fetch(new URL(url)).then(r => r.text());
+ } catch (e) {
+ assert(e instanceof Error);
+ assert(match);
+ showLoadingError(
+ `Failed to parse the URL in the "/${match[1]
+ }# sourceMappingURL=" comment: ${(e && e.message) || e}`,
+ );
+ return;
+ }
+ finishLoading(code, map);
+ } else if (file && isProbablySourceMap(file)) {
+ // Allow loading a source map without a generated file because why not
+ finishLoading('', code);
+ } else {
+ const c = file && file.name.endsWith('ss') ? '*' : '/';
+ showLoadingError(
+ `Failed to find an embedded "/${c}# sourceMappingURL=" comment in the ${file ? 'imported file' : 'pasted text'
+ }.`,
+ );
+ }
+}
+
+async function startLoading(files: FileList) {
+ if (files.length === 1) {
+ const file0 = files[0];
+ const code = await loadFile(file0);
+ assert(code);
+ finishLoadingCodeWithEmbeddedSourceMap(code, file0);
+ } else if (files.length === 2) {
+ const file0 = files[0];
+ const file1 = files[1];
+
+ if (isProbablySourceMap(file0)) {
+ const codePromise = loadFile(file1);
+ const mapPromise = loadFile(file0);
+ const code = await codePromise;
+ const map = await mapPromise;
+ assert(code && map);
+ finishLoading(code, map);
+ } else if (isProbablySourceMap(file1)) {
+ const codePromise = loadFile(file0);
+ const mapPromise = loadFile(file1);
+ const code = await codePromise;
+ const map = await mapPromise;
+ assert(code && map);
+ finishLoading(code, map);
+ } else {
+ showLoadingError(
+ `The source map file must end in either ".map" or ".json" to be detected.`,
+ );
+ }
+ } else {
+ showLoadingError(`Please import either 1 or 2 files.`);
+ }
+}
+
+document.body.addEventListener('paste', e => {
+ e.preventDefault();
+ const { clipboardData } = e;
+ assert(clipboardData);
+ const code = clipboardData.getData('text/plain');
+ finishLoadingCodeWithEmbeddedSourceMap(code, null);
+});
+
+// Accelerate VLQ decoding with a lookup table
+const vlqTable = new Uint8Array(128);
+const vlqChars =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+for (let i = 0; i < vlqTable.length; i++) vlqTable[i] = 0xff;
+for (let i = 0; i < vlqChars.length; i++) vlqTable[vlqChars.charCodeAt(i)] = i;
+
+function decodeMappings(
+ mappings: string,
+ sourcesCount: number,
+ namesCount: number,
+) {
+ const n = mappings.length;
+ let data = new Int32Array(1024);
+ let dataLength = 0;
+ let generatedLine = 0;
+ let generatedLineStart = 0;
+ let generatedColumn = 0;
+ let originalSource = 0;
+ let originalLine = 0;
+ let originalColumn = 0;
+ let originalName = 0;
+ let needToSortGeneratedColumns = false;
+ let i = 0;
+
+ function decodeError(text: string) {
+ const error = `Invalid VLQ data at index ${i}: ${text}`;
+ showLoadingError(
+ `The "mappings" field of the imported source map contains invalid data. ${error}.`,
+ );
+ throw new Error(error);
+ }
+
+ function decodeVLQ() {
+ let shift = 0;
+ let vlq = 0;
+
+ // Scan over the input
+ while (true) {
+ // Read a byte
+ if (i >= mappings.length)
+ decodeError('Unexpected early end of mapping data');
+ const c = mappings.charCodeAt(i);
+ if ((c & 0x7f) !== c)
+ decodeError(
+ `Invalid mapping character: ${JSON.stringify(
+ String.fromCharCode(c),
+ )}`,
+ );
+ const index = vlqTable[c & 0x7f];
+ if (index === 0xff)
+ decodeError(
+ `Invalid mapping character: ${JSON.stringify(
+ String.fromCharCode(c),
+ )}`,
+ );
+ i++;
+
+ // Decode the byte
+ vlq |= (index & 31) << shift;
+ shift += 5;
+
+ // Stop if there's no continuation bit
+ if ((index & 32) === 0) break;
+ }
+
+ // Recover the signed value
+ return vlq & 1 ? -(vlq >> 1) : vlq >> 1;
+ }
+
+ while (i < n) {
+ let c = mappings.charCodeAt(i);
+
+ // Handle a line break
+ if (c === 59 /* ; */) {
+ // The generated columns are very rarely out of order. In that case,
+ // sort them with insertion since they are very likely almost ordered.
+ if (needToSortGeneratedColumns) {
+ for (let j = generatedLineStart + 6; j < dataLength; j += 6) {
+ const genL = data[j];
+ const genC = data[j + 1];
+ const origS = data[j + 2];
+ const origL = data[j + 3];
+ const origC = data[j + 4];
+ const origN = data[j + 5];
+ let k = j - 6;
+ for (; k >= generatedLineStart && data[k + 1] > genC; k -= 6) {
+ data[k + 6] = data[k];
+ data[k + 7] = data[k + 1];
+ data[k + 8] = data[k + 2];
+ data[k + 9] = data[k + 3];
+ data[k + 10] = data[k + 4];
+ data[k + 11] = data[k + 5];
+ }
+ data[k + 6] = genL;
+ data[k + 7] = genC;
+ data[k + 8] = origS;
+ data[k + 9] = origL;
+ data[k + 10] = origC;
+ data[k + 11] = origN;
+ }
+ }
+
+ generatedLine++;
+ generatedColumn = 0;
+ generatedLineStart = dataLength;
+ needToSortGeneratedColumns = false;
+ i++;
+ continue;
+ }
+
+ // Ignore stray commas
+ if (c === 44 /* , */) {
+ i++;
+ continue;
+ }
+
+ // Read the generated column
+ const generatedColumnDelta = decodeVLQ();
+ if (generatedColumnDelta < 0) needToSortGeneratedColumns = true;
+ generatedColumn += generatedColumnDelta;
+ if (generatedColumn < 0)
+ decodeError(`Invalid generated column: ${generatedColumn}`);
+
+ // It's valid for a mapping to have 1, 4, or 5 variable-length fields
+ let isOriginalSourceMissing = true;
+ let isOriginalNameMissing = true;
+ if (i < n) {
+ c = mappings.charCodeAt(i);
+ if (c === 44 /* , */) {
+ i++;
+ } else if (c !== 59 /* ; */) {
+ isOriginalSourceMissing = false;
+
+ // Read the original source
+ const originalSourceDelta = decodeVLQ();
+ originalSource += originalSourceDelta;
+ if (originalSource < 0 || originalSource >= sourcesCount)
+ decodeError(
+ `Original source index ${originalSource} is invalid (there are ${sourcesCount} sources)`,
+ );
+
+ // Read the original line
+ const originalLineDelta = decodeVLQ();
+ originalLine += originalLineDelta;
+ if (originalLine < 0)
+ decodeError(`Invalid original line: ${originalLine}`);
+
+ // Read the original column
+ const originalColumnDelta = decodeVLQ();
+ originalColumn += originalColumnDelta;
+ if (originalColumn < 0)
+ decodeError(`Invalid original column: ${originalColumn}`);
+
+ // Check for the optional name index
+ if (i < n) {
+ c = mappings.charCodeAt(i);
+ if (c === 44 /* , */) {
+ i++;
+ } else if (c !== 59 /* ; */) {
+ isOriginalNameMissing = false;
+
+ // Read the optional name index
+ const originalNameDelta = decodeVLQ();
+ originalName += originalNameDelta;
+ if (originalName < 0 || originalName >= namesCount)
+ decodeError(
+ `Original name index ${originalName} is invalid (there are ${namesCount} names)`,
+ );
+
+ // Handle the next character
+ if (i < n) {
+ c = mappings.charCodeAt(i);
+ if (c === 44 /* , */) {
+ i++;
+ } else if (c !== 59 /* ; */) {
+ decodeError(
+ `Invalid character after mapping: ${JSON.stringify(
+ String.fromCharCode(c),
+ )}`,
+ );
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Append the mapping to the typed array
+ if (dataLength + 6 > data.length) {
+ const newData = new Int32Array(data.length << 1);
+ newData.set(data);
+ data = newData;
+ }
+ data[dataLength] = generatedLine;
+ data[dataLength + 1] = generatedColumn;
+ if (isOriginalSourceMissing) {
+ data[dataLength + 2] = -1;
+ data[dataLength + 3] = -1;
+ data[dataLength + 4] = -1;
+ } else {
+ data[dataLength + 2] = originalSource;
+ data[dataLength + 3] = originalLine;
+ data[dataLength + 4] = originalColumn;
+ }
+ data[dataLength + 5] = isOriginalNameMissing ? -1 : originalName;
+ dataLength += 6;
+ }
+
+ return data.subarray(0, dataLength);
+}
+
+interface Source {
+ name: string;
+ content: string;
+ data: Int32Array;
+ dataLength: number;
+}
+
+function generateInverseMappings(sources: Source[], data: Int32Array) {
+ let longestDataLength = 0;
+
+ // Scatter the mappings to the individual sources
+ for (let i = 0, n = data.length; i < n; i += 6) {
+ const originalSource = data[i + 2];
+ if (originalSource === -1) continue;
+
+ const source = sources[originalSource];
+ let inverseData = source.data;
+ let j = source.dataLength;
+
+ // Append the mapping to the typed array
+ if (j + 6 > inverseData.length) {
+ const newLength = inverseData.length << 1;
+ const newData = new Int32Array(newLength > 1024 ? newLength : 1024);
+ newData.set(inverseData);
+ source.data = inverseData = newData;
+ }
+ inverseData[j] = data[i];
+ inverseData[j + 1] = data[i + 1];
+ inverseData[j + 2] = originalSource;
+ inverseData[j + 3] = data[i + 3];
+ inverseData[j + 4] = data[i + 4];
+ inverseData[j + 5] = data[i + 5];
+ j += 6;
+ source.dataLength = j;
+ if (j > longestDataLength) longestDataLength = j;
+ }
+
+ // Sort the mappings for each individual source
+ const temp = new Int32Array(longestDataLength);
+ for (const source of sources) {
+ const data = source.data.subarray(0, source.dataLength);
+
+ // Sort lazily for performance
+ let isSorted = false;
+ Object.defineProperty(source, 'data', {
+ get() {
+ if (!isSorted) {
+ temp.set(data);
+ topDownSplitMerge(temp, 0, data.length, data);
+ isSorted = true;
+ }
+ return data;
+ },
+ });
+ }
+
+ // From: https://en.wikipedia.org/wiki/Merge_sort
+ function topDownSplitMerge(
+ B: Int32Array,
+ iBegin: number,
+ iEnd: number,
+ A: Int32Array,
+ ) {
+ if (iEnd - iBegin <= 6) return;
+
+ // Optimization: Don't do merge sort if it's already sorted
+ let isAlreadySorted = true;
+ for (let i = iBegin + 3, j = i + 6; j < iEnd; i = j, j += 6) {
+ // Compare mappings first by original line (index 3) and then by original column (index 4)
+ if (A[i] < A[j] || (A[i] === A[j] && A[i + 1] <= A[j + 1])) continue;
+ isAlreadySorted = false;
+ break;
+ }
+ if (isAlreadySorted) {
+ return;
+ }
+
+ const iMiddle = ((iEnd / 6 + iBegin / 6) >> 1) * 6;
+ topDownSplitMerge(A, iBegin, iMiddle, B);
+ topDownSplitMerge(A, iMiddle, iEnd, B);
+ topDownMerge(B, iBegin, iMiddle, iEnd, A);
+ }
+
+ // From: https://en.wikipedia.org/wiki/Merge_sort
+ function topDownMerge(
+ A: Int32Array,
+ iBegin: number,
+ iMiddle: number,
+ iEnd: number,
+ B: Int32Array,
+ ) {
+ let i = iBegin,
+ j = iMiddle;
+ for (let k = iBegin; k < iEnd; k += 6) {
+ if (
+ i < iMiddle &&
+ (j >= iEnd ||
+ // Compare mappings first by original line (index 3) and then by original column (index 4)
+ A[i + 3] < A[j + 3] ||
+ (A[i + 3] === A[j + 3] && A[i + 4] <= A[j + 4]))
+ ) {
+ B[k] = A[i];
+ B[k + 1] = A[i + 1];
+ B[k + 2] = A[i + 2];
+ B[k + 3] = A[i + 3];
+ B[k + 4] = A[i + 4];
+ B[k + 5] = A[i + 5];
+ i += 6;
+ } else {
+ B[k] = A[j];
+ B[k + 1] = A[j + 1];
+ B[k + 2] = A[j + 2];
+ B[k + 3] = A[j + 3];
+ B[k + 4] = A[j + 4];
+ B[k + 5] = A[j + 5];
+ j += 6;
+ }
+ }
+ }
+}
+
+function parseSourceMap(raw: string) {
+ let json: RawIndexMap | RawSourceMap;
+ try {
+ json = JSON.parse(raw);
+ assert(json instanceof Object);
+ assert(!(json instanceof Array));
+ } catch (e) {
+ assert(e instanceof Error);
+ showLoadingError(
+ `The imported source map contains invalid JSON data: ${(e && e.message) || e
+ }`,
+ );
+ throw e;
+ }
+
+ if (json.version !== 3) {
+ showLoadingError(
+ `The imported source map is invalid. Expected the "version" field to contain the number 3.`,
+ );
+ throw new Error('Invalid source map');
+ }
+
+ if ('sections' in json && json.sections instanceof Array) {
+ const sections = json.sections;
+ const decodedSections = [];
+ let totalDataLength = 0;
+
+ for (let i = 0; i < sections.length; i++) {
+ const {
+ offset: { line, column },
+ map,
+ } = sections[i];
+ if (typeof line !== 'number' || typeof column !== 'number') {
+ showLoadingError(
+ `The imported source map is invalid. Expected the "offset" field for section ${i} to have a line and column.`,
+ );
+ throw new Error('Invalid source map');
+ }
+
+ if (!map) {
+ showLoadingError(
+ `The imported source map is unsupported. Section ${i} does not contain a "map" field.`,
+ );
+ throw new Error('Invalid source map');
+ }
+
+ if (map.version !== 3) {
+ showLoadingError(
+ `The imported source map is invalid. Expected the "version" field for section ${i} to contain the number 3.`,
+ );
+ throw new Error('Invalid source map');
+ }
+
+ if (
+ !(map.sources instanceof Array) ||
+ map.sources.some(x => typeof x !== 'string')
+ ) {
+ showLoadingError(
+ `The imported source map is invalid. Expected the "sources" field for section ${i} to be an array of strings.`,
+ );
+ throw new Error('Invalid source map');
+ }
+
+ if (typeof map.mappings !== 'string') {
+ showLoadingError(
+ `The imported source map is invalid. Expected the "mappings" field for section ${i} to be a string.`,
+ );
+ throw new Error('Invalid source map');
+ }
+
+ const { sourcesContent, names, mappings } = map;
+ let sources: Source[] = [];
+ const emptyData = new Int32Array(0);
+ for (let i = 0; i < map.sources.length; i++) {
+ sources[i] = {
+ name: map.sources[i],
+ content: (sourcesContent && sourcesContent[i]) || '',
+ data: emptyData,
+ dataLength: 0,
+ };
+ }
+
+ const data = decodeMappings(
+ mappings,
+ sources.length,
+ names ? names.length : 0,
+ );
+ decodedSections.push({ offset: { line, column }, sources, names, data });
+ totalDataLength += data.length;
+ }
+
+ decodedSections.sort((a, b) => {
+ if (a.offset.line < b.offset.line) return -1;
+ if (a.offset.line > b.offset.line) return 1;
+ if (a.offset.column < b.offset.column) return -1;
+ if (a.offset.column > b.offset.column) return 1;
+ return 0;
+ });
+
+ const mergedData = new Int32Array(totalDataLength);
+ const mergedSources = [];
+ const mergedNames = [];
+ let dataOffset = 0;
+
+ for (const {
+ offset: { line, column },
+ sources,
+ names,
+ data,
+ } of decodedSections) {
+ const sourcesOffset = mergedSources.length;
+ const nameOffset = mergedNames.length;
+
+ for (let i = 0, n = data.length; i < n; i += 6) {
+ if (data[i] === 0) data[i + 1] += column;
+ data[i] += line;
+ if (data[i + 2] !== -1) data[i + 2] += sourcesOffset;
+ if (data[i + 5] !== -1) data[i + 5] += nameOffset;
+ }
+
+ mergedData.set(data, dataOffset);
+ for (const source of sources) mergedSources.push(source);
+ if (names) for (const name of names) mergedNames.push(name);
+ dataOffset += data.length;
+ }
+
+ generateInverseMappings(mergedSources, mergedData);
+ return {
+ sources: mergedSources,
+ names: mergedNames,
+ data: mergedData,
+ };
+ }
+
+ if (
+ !('sources' in json) ||
+ !(json.sources instanceof Array) ||
+ json.sources.some(x => typeof x !== 'string')
+ ) {
+ showLoadingError(
+ `The imported source map is invalid. Expected the "sources" field to be an array of strings.`,
+ );
+ throw new Error('Invalid source map');
+ }
+
+ if (typeof json.mappings !== 'string') {
+ showLoadingError(
+ `The imported source map is invalid. Expected the "mappings" field to be a string.`,
+ );
+ throw new Error('Invalid source map');
+ }
+
+ const { sourcesContent, names, mappings } = json;
+ let sources: Source[] = [];
+ const emptyData = new Int32Array(0);
+ for (let i = 0; i < json.sources.length; i++) {
+ sources[i] = {
+ name: json.sources[i],
+ content: (sourcesContent && sourcesContent[i]) || '',
+ data: emptyData,
+ dataLength: 0,
+ };
+ }
+
+ const data = decodeMappings(
+ mappings,
+ sources.length,
+ names ? names.length : 0,
+ );
+ generateInverseMappings(sources, data);
+ return { sources, names, data };
+}
+
+const toolbarHeight = 32;
+const statusBarHeight = 32;
+
+function waitForDOM() {
+ return new Promise(r => setTimeout(r, 1));
+}
+
+async function finishLoading(code: string, map: string) {
+ const startTime = Date.now();
+ promptText.style.display = 'none';
+ toolbar.style.display = 'flex';
+ statusBar.style.display = 'flex';
+ canvas.style.display = 'block';
+ originalStatus.textContent = generatedStatus.textContent = '';
+ fileList.innerHTML = '';
+ const option = document.createElement('option');
+ option.textContent = `Loading...`;
+ fileList.appendChild(option);
+ fileList.disabled = true;
+ fileList.selectedIndex = 0;
+ originalTextArea = generatedTextArea = hover = null;
+ isInvalid = true;
+ updateHash(code, map);
+
+ // Let the browser update before parsing the source map, which may be slow
+ await waitForDOM();
+ const sm = parseSourceMap(map);
+
+ // Show a progress bar if this is is going to take a while
+ let charsSoFar = 0;
+ let progressCalls = 0;
+ let isProgressVisible = false;
+ const progressStart = Date.now();
+ const totalChars =
+ code.length + (sm.sources.length > 0 ? sm.sources[0].content.length : 0);
+ const progress = (chars: number) => {
+ charsSoFar += chars;
+ if (!isProgressVisible && progressCalls++ > 2 && charsSoFar) {
+ const estimatedTimeLeftMS =
+ ((Date.now() - progressStart) / charsSoFar) * (totalChars - charsSoFar);
+ if (estimatedTimeLeftMS > 250) {
+ progressBarOverlay.style.display = 'block';
+ isProgressVisible = true;
+ }
+ }
+ if (isProgressVisible) {
+ progressBar.style.transform = `scaleX(${charsSoFar / totalChars})`;
+ return waitForDOM();
+ }
+ };
+ progressBar.style.transform = `scaleX(0)`;
+
+ // Update the original text area when the source changes
+ const otherSource = (index: number) =>
+ index === -1 ? null : sm.sources[index].name;
+ const originalName = (index: number) => sm.names[index];
+ let finalOriginalTextArea = null;
+ if (sm.sources.length > 0) {
+ const updateOriginalSource = (
+ sourceIndex: number,
+ progress?: ProgressCallback,
+ ) => {
+ const source = sm.sources[sourceIndex];
+ return createTextArea({
+ sourceIndex,
+ text: source.content,
+ progress,
+ mappings: source.data,
+ mappingsOffset: 3,
+ otherSource,
+ originalName,
+ bounds() {
+ return {
+ x: 0,
+ y: toolbarHeight,
+ width: (innerWidth >>> 1) - (splitterWidth >> 1),
+ height: innerHeight - toolbarHeight - statusBarHeight,
+ };
+ },
+ });
+ };
+ fileList.onchange = async () => {
+ originalTextArea = await updateOriginalSource(fileList.selectedIndex);
+ isInvalid = true;
+ };
+ finalOriginalTextArea = await updateOriginalSource(0, progress);
+ }
+
+ generatedTextArea = await createTextArea({
+ sourceIndex: null,
+ text: code,
+ progress,
+ mappings: sm.data,
+ mappingsOffset: 0,
+ otherSource,
+ originalName,
+ bounds() {
+ const x = (innerWidth >> 1) + ((splitterWidth + 1) >> 1);
+ return {
+ x,
+ y: toolbarHeight,
+ width: innerWidth - x,
+ height: innerHeight - toolbarHeight - statusBarHeight,
+ };
+ },
+ });
+
+ // Only render the original text area once the generated text area is ready
+ originalTextArea = finalOriginalTextArea;
+ isInvalid = true;
+
+ // Populate the file picker once there will be no more await points
+ fileList.innerHTML = '';
+ if (sm.sources.length > 0) {
+ for (let sources = sm.sources, i = 0, n = sources.length; i < n; i++) {
+ const option = document.createElement('option');
+ option.textContent = `${i}: ${sources[i].name}`;
+ fileList.appendChild(option);
+ }
+ fileList.disabled = false;
+ } else {
+ const option = document.createElement('option');
+ option.textContent = `(no original code)`;
+ fileList.appendChild(option);
+ }
+ fileList.selectedIndex = 0;
+
+ if (isProgressVisible) progressBarOverlay.style.display = 'none';
+ const endTime = Date.now();
+ console.log(`Finished loading in ${endTime - startTime}ms`);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Drawing
+interface Mapping {
+ generatedLine: number;
+ generatedColumn: number;
+ originalSource: number;
+ originalLine: number;
+ originalColumn: number;
+ originalName: number;
+}
+interface Hover {
+ sourceIndex: number | null;
+ lineIndex: number;
+ row: number;
+ column: number;
+ index: number;
+ mapping?: Mapping;
+}
+interface Bounds {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+}
+type ProgressCallback = (chars: number) => Promisable;
+interface TextArea {
+ sourceIndex: number | null;
+ updateAfterWrapChange(): void;
+ getHoverRect(): [x: number, y: number, w: number, h: number] | null;
+ bounds(): Bounds;
+ draw(bodyStyle: CSSStyleDeclaration): void;
+ scrollTo(column: number, line: number): void;
+ onmousemove(e: MouseEvent): void;
+ onmousedown(e: MouseEvent): void;
+ onwheel(e: WheelEvent): void;
+}
+
+const originalLineColors = [
+ 'rgba(25, 133, 255, 0.3)', // Blue
+ 'rgba(174, 97, 174, 0.3)', // Purple
+ 'rgba(255, 97, 106, 0.3)', // Red
+ 'rgba(250, 192, 61, 0.3)', // Yellow
+ 'rgba(115, 192, 88, 0.3)', // Green
+];
+
+// Use a striped pattern for bad mappings (good mappings are solid)
+const patternContours = [
+ [0, 24, 24, 0, 12, 0, 0, 12, 0, 24],
+ [0, 28, 28, 0, 40, 0, 0, 40, 0, 28],
+ [0, 44, 44, 0, 56, 0, 0, 56, 0, 44],
+ [12, 64, 24, 64, 64, 24, 64, 12, 12, 64],
+ [0, 60, 0, 64, 8, 64, 64, 8, 64, 0, 60, 0, 0, 60],
+ [28, 64, 40, 64, 64, 40, 64, 28, 28, 64],
+ [0, 8, 8, 0, 0, 0, 0, 8],
+ [44, 64, 56, 64, 64, 56, 64, 44, 44, 64],
+ [64, 64, 64, 60, 60, 64, 64, 64],
+];
+const badMappingPatterns = originalLineColors.map(color => {
+ let patternCanvas = document.createElement('canvas');
+ let patternContext = patternCanvas.getContext('2d');
+ assert(patternContext);
+ let ratio: number, scale: number, pattern: CanvasPattern;
+ return (dx: number, dy: number) => {
+ if (devicePixelRatio !== ratio) {
+ ratio = devicePixelRatio;
+ scale = Math.round(64 * ratio) / 64;
+ patternCanvas.width = patternCanvas.height = Math.round(64 * scale);
+ patternContext.scale(scale, scale);
+ patternContext.beginPath();
+ for (const contour of patternContours) {
+ for (let i = 0; i < contour.length; i += 2) {
+ if (i === 0) patternContext.moveTo(contour[i], contour[i + 1]);
+ else patternContext.lineTo(contour[i], contour[i + 1]);
+ }
+ }
+ patternContext.fillStyle = color.replace(' 0.3)', ' 0.2)');
+ patternContext.fill();
+ pattern = $assert(c.createPattern(patternCanvas, 'repeat'));
+ }
+ pattern.setTransform(new DOMMatrix([1 / scale, 0, 0, 1 / scale, dx, dy]));
+ return pattern;
+ };
+});
+
+const canvas = document.createElement('canvas');
+const c = $assert(canvas.getContext('2d'));
+const monospaceFont = '14px monospace';
+const rowHeight = 21;
+const splitterWidth = 6;
+const margin = 64;
+let isInvalid = true;
+let originalTextArea: TextArea | null = null;
+let generatedTextArea: TextArea | null = null;
+let hover: Hover | null = null;
+let highlighted: Record<
+ 'sourceIndex' | 'startIndex' | 'startColumn' | 'endIndex' | 'endColumn',
+ number
+> | null = {
+ sourceIndex: 0,
+ startIndex: 5,
+ startColumn: 5,
+ endIndex: 7,
+ endColumn: 11,
+};
+
+const wrapCheckbox = $('input#wrap');
+let wrap = true;
+try {
+ wrap = localStorage.getItem('wrap') !== 'false';
+} catch (e) { }
+wrapCheckbox.checked = wrap;
+wrapCheckbox.onchange = () => {
+ wrap = wrapCheckbox.checked;
+ try {
+ localStorage.setItem('wrap', String(wrap));
+ } catch (e) { }
+ if (originalTextArea) originalTextArea.updateAfterWrapChange();
+ if (generatedTextArea) generatedTextArea.updateAfterWrapChange();
+ isInvalid = true;
+};
+
+interface Line {
+ raw: string;
+ runBase: number;
+ runCount: number;
+ runText: Record;
+ endIndex: number;
+ endColumn: number;
+}
+
+async function splitTextIntoLinesAndRuns(
+ text: string,
+ progress?: ProgressCallback,
+) {
+ c.font = monospaceFont;
+ const spaceWidth = c.measureText(' ').width;
+ const spacesPerTab = 2;
+ const parts = text.split(/(\r\n|\r|\n)/g);
+ const unicodeWidthCache = new Map();
+ const lines: Line[] = [];
+ const progressChunkSize = 1 << 20;
+ let longestColumnForLine = new Int32Array(1024);
+ let runData = new Int32Array(1024);
+ let runDataLength = 0;
+ let prevProgressPoint = 0;
+ let longestLineInColumns = 0;
+ let lineStartOffset = 0;
+
+ for (let part = 0; part < parts.length; part++) {
+ let raw = parts[part];
+ if (part & 1) {
+ // Accumulate the length of the newline (CRLF uses two code units)
+ lineStartOffset += raw.length;
+ continue;
+ }
+
+ const runBase = runDataLength;
+ const n = raw.length + 1; // Add 1 for the extra character at the end
+ let nextProgressPoint = progress
+ ? prevProgressPoint + progressChunkSize - lineStartOffset
+ : Infinity;
+ let i = 0;
+ let column = 0;
+
+ while (i < n) {
+ let startIndex = i;
+ let startColumn = column;
+ let whitespace = 0;
+ let isSingleChunk = false;
+
+ // Update the progress bar occasionally
+ if (i > nextProgressPoint) {
+ assert(progress);
+ await progress(lineStartOffset + i - prevProgressPoint);
+ prevProgressPoint = lineStartOffset + i;
+ nextProgressPoint = i + progressChunkSize;
+ }
+
+ while (i < n) {
+ let c1 = raw.charCodeAt(i);
+ let c2;
+
+ // Draw each tab into its own run
+ if (c1 === 0x09 /* tab */) {
+ if (i > startIndex) break;
+ isSingleChunk = true;
+ column += spacesPerTab;
+ column -= column % spacesPerTab;
+ i++;
+ whitespace = c1;
+ break;
+ }
+
+ // Draw each newline into its own run
+ if (c1 !== c1 /* end of line */) {
+ if (i > startIndex) break;
+ isSingleChunk = true;
+ column++;
+ i++;
+ whitespace = 0x0a /* newline */;
+ break;
+ }
+
+ // Draw each non-ASCII character into its own run (e.g. emoji)
+ if (c1 < 0x20 || c1 > 0x7e) {
+ if (i > startIndex) break;
+ isSingleChunk = true;
+ i++;
+
+ // Consume another code unit if this code unit is a high surrogate
+ // and the next code point is a low surrogate. This handles code
+ // points that span two UTF-16 code units.
+ if (
+ i < n &&
+ c1 >= 0xd800 &&
+ c1 <= 0xdbff &&
+ (c2 = raw.charCodeAt(i)) >= 0xdc00 &&
+ c2 <= 0xdfff
+ ) {
+ i++;
+ }
+
+ // This contains some logic to handle more complex emoji such as "๐ฏโโ๏ธ"
+ // which is [U+1F46F, U+200D, U+2642, U+FE0F].
+ while (i < n) {
+ c1 = raw.charCodeAt(i);
+
+ // Consume another code unit if the next code point is a variation selector
+ if ((c1 & ~0xf) === 0xfe00) {
+ i++;
+ }
+
+ // Consume another code unit if the next code point is a skin tone modifier
+ else if (
+ c1 === 0xd83c &&
+ i + 1 < n &&
+ (c2 = raw.charCodeAt(i + 1)) >= 0xdffb &&
+ c2 <= 0xdfff
+ ) {
+ i += 2;
+ }
+
+ // Consume another code unit and stop if the next code point is a zero-width non-joiner
+ else if (c1 === 0x200c) {
+ i++;
+ break;
+ }
+
+ // Consume another code unit if the next code point is a zero-width joiner
+ else if (c1 === 0x200d) {
+ i++;
+
+ // Consume the next code point that is "joined" to this one
+ if (i < n) {
+ c1 = raw.charCodeAt(i);
+ i++;
+ if (
+ c1 >= 0xd800 &&
+ c1 <= 0xdbff &&
+ i < n &&
+ (c2 = raw.charCodeAt(i)) >= 0xdc00 &&
+ c2 <= 0xdfff
+ ) {
+ i++;
+ }
+ }
+ } else {
+ break;
+ }
+ }
+
+ const key = raw.slice(startIndex, i);
+ let width = unicodeWidthCache.get(key);
+ if (width === void 0) {
+ width = Math.round(c.measureText(key).width / spaceWidth);
+ if (width < 1) width = 1;
+ unicodeWidthCache.set(key, width);
+ }
+ column += width;
+ break;
+ }
+
+ // Draw runs of spaces in their own run
+ if (c1 === 0x20 /* space */) {
+ if (i === startIndex) whitespace = c1;
+ else if (!whitespace) break;
+ } else {
+ if (whitespace) break;
+ }
+
+ column++;
+ i++;
+ }
+
+ // Append the run to the typed array
+ if (runDataLength + 5 > runData.length) {
+ const newData = new Int32Array(runData.length << 1);
+ newData.set(runData);
+ runData = newData;
+ }
+ runData[runDataLength] =
+ whitespace | (isSingleChunk ? 0x100 /* isSingleChunk */ : 0);
+ runData[runDataLength + 1] = startIndex;
+ runData[runDataLength + 2] = i;
+ runData[runDataLength + 3] = startColumn;
+ runData[runDataLength + 4] = column;
+ runDataLength += 5;
+ }
+
+ const lineIndex = lines.length;
+ if (lineIndex >= longestColumnForLine.length) {
+ const newData = new Int32Array(longestColumnForLine.length << 1);
+ newData.set(longestColumnForLine);
+ longestColumnForLine = newData;
+ }
+ longestColumnForLine[lineIndex] = column;
+
+ const runCount = (runDataLength - runBase) / 5;
+ lines.push({
+ raw,
+ runBase,
+ runCount,
+ runText: {},
+ endIndex: i,
+ endColumn: column,
+ });
+ longestLineInColumns = Math.max(longestLineInColumns, column);
+ lineStartOffset += raw.length;
+ }
+
+ if (prevProgressPoint < text.length && progress) {
+ await progress(text.length - prevProgressPoint);
+ }
+
+ return {
+ lines,
+ longestColumnForLine,
+ longestLineInColumns,
+ runData: runData.subarray(0, runDataLength),
+ };
+}
+
+interface CreateTextAreaParams {
+ sourceIndex: number | null;
+ text: string;
+ progress?: ProgressCallback;
+ mappings: Int32Array;
+ mappingsOffset: number;
+ otherSource: (index: number) => string | null;
+ originalName: (index: number) => string;
+ bounds: () => Bounds;
+}
+
+async function createTextArea({
+ sourceIndex,
+ text,
+ progress,
+ mappings,
+ mappingsOffset,
+ otherSource,
+ originalName,
+ bounds,
+}: CreateTextAreaParams): Promise