index.tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. import React, { FC, useRef, useState } from 'react';
  2. import Highlight, { DefaultProps } from 'prism-react-renderer';
  3. import Prism from './prism';
  4. import './prism';
  5. import './prism.css';
  6. import exec from './sandbox';
  7. import ts from 'typescript';
  8. const canStream = 'stream' in File.prototype;
  9. type Preset = {
  10. fflate: string;
  11. uzip: string;
  12. pako: string;
  13. };
  14. const presets: Record<string, Preset> = {
  15. 'Basic GZIP compression': {
  16. fflate: `
  17. var file = files[0];
  18. fileToU8(file, function(buf) {
  19. fflate.gzip(buf, {
  20. level: 6,
  21. // These are optional, but fflate supports the metadata
  22. mtime: file.lastModified,
  23. filename: file.name
  24. }, function(err, data) {
  25. // Hope you're not causing any errors in the demo ;)
  26. callback(data);
  27. });
  28. });`,
  29. uzip: `
  30. var file = files[0];
  31. fileToU8(file, function(buf) {
  32. // UZIP doesn't natively support GZIP, but I patched in support for it.
  33. // In other words, you're better off using fflate for GZIP.
  34. // Also, UZIP runs synchronously on the main thread. It relies on global
  35. // state, so you can't even run it in the background without causing bugs.
  36. // But just for the sake of a performance comparison, try it out.
  37. uzipWorker.gzip(buf, function(err, data) {
  38. callback(data);
  39. });
  40. });`,
  41. pako: `
  42. var file = files[0];
  43. fileToU8(file, function(buf) {
  44. // Unlike UZIP, Pako natively supports GZIP, and it doesn't rely on global
  45. // state. However, it's still 46kB for this basic functionality as opposed
  46. // to fflate's 7kB, not to mention the fact that there's no easy way to use
  47. // it asynchronously. I had to add a worker proxy for this to work.
  48. pakoWorker.gzip(buf, function(err, data) {
  49. callback(data);
  50. });
  51. }); `
  52. }
  53. }
  54. if (canStream) {
  55. presets['Streaming GZIP compression'] = {
  56. fflate: `
  57. const { AsyncGzip } = fflate;
  58. const file = files[0];
  59. const gzipStream = new AsyncGzip({ level: 6 });
  60. // We can stream the file through GZIP to reduce memory usage
  61. const fakeResponse = new Response(
  62. file.stream().pipeThrough(toNativeStream(gzipStream))
  63. );
  64. fakeResponse.arrayBuffer().then(buf => {
  65. callback(new Uint8Array(buf));
  66. });`,
  67. uzip: `
  68. // UZIP doesn't support streaming to any extent`,
  69. pako: `
  70. // Hundreds of lines of code to make this run on a Worker...
  71. const file = files[0];
  72. // In case this wasn't clear already, Pako doesn't actually support this,
  73. // you need to create a custom async stream. I suppose you could copy the
  74. // code used in this demo.
  75. const gzipStream = pakoWorker.createGzip();
  76. const fakeResponse = new Response(
  77. file.stream().pipeThrough(toNativeStream(gzipStream))
  78. );
  79. fakeResponse.arrayBuffer().then(buf => {
  80. callback(new Uint8Array(buf));
  81. });`
  82. };
  83. }
  84. const CodeHighlight: FC<{
  85. code: string;
  86. onInput: (newCode: string) => void;
  87. }> = ({ code, onInput }) => {
  88. const tmpParen = useRef(-1);
  89. return (
  90. <>
  91. <pre className="language-javascript" style={{
  92. width: '100%',
  93. height: '100%',
  94. position: 'relative',
  95. backgroundColor: '#2a2734',
  96. color: '#9a86fd',
  97. fontSize: '0.7em'
  98. }}>
  99. <div>
  100. <Highlight Prism={Prism.Prism as unknown as DefaultProps['Prism']} code={code} language="javascript">
  101. {({ tokens, getLineProps, getTokenProps }) => (
  102. tokens.map((line, i) => (
  103. <div {...getLineProps({ line, key: i })}>
  104. {line.map((token, key) => (
  105. <span {...getTokenProps({ token, key })} />
  106. ))}
  107. </div>
  108. ))
  109. )}
  110. </Highlight>
  111. </div>
  112. <textarea
  113. autoComplete="off"
  114. autoCorrect="off"
  115. autoCapitalize="off"
  116. spellCheck="false"
  117. style={{
  118. border: 'unset',
  119. resize: 'none',
  120. outline: 'none',
  121. position: 'absolute',
  122. background: 'transparent',
  123. top: 0,
  124. left: 0,
  125. width: '100%',
  126. height: '100%',
  127. lineHeight: 'inherit',
  128. fontSize: 'inherit',
  129. padding: 'inherit',
  130. color: 'transparent',
  131. caretColor: 'white',
  132. fontFamily: 'inherit'
  133. }}
  134. onKeyDown={e => {
  135. const t = e.currentTarget;
  136. let val = t.value;
  137. const loc = t.selectionStart;
  138. let newTmpParen = -1;
  139. if (e.key == 'Enter') {
  140. const lastNL = val.lastIndexOf('\n', loc - 1);
  141. let indent = 0;
  142. for (; val.charCodeAt(indent + lastNL + 1) == 32; ++indent);
  143. const lastChar = val.charAt(loc - 1);
  144. const nextChar = val.charAt(loc);
  145. if (lastChar == '{'|| lastChar == '(' || lastChar == '[') indent += 2;
  146. const addNL = nextChar == '}' || nextChar == ')' || nextChar == ']';
  147. const tail = val.slice(t.selectionEnd);
  148. val = val.slice(0, loc) + '\n';
  149. for (let i = 0; i < indent; ++i) val += ' ';
  150. if (addNL) {
  151. if (
  152. (lastChar == '{' && nextChar == '}') ||
  153. (lastChar == '[' && nextChar == ']') ||
  154. (lastChar == '(' && nextChar == ')')
  155. ) {
  156. val += '\n';
  157. for (let i = 2; i < indent; ++i) val += ' ';
  158. } else {
  159. const end = Math.min(indent, 2);
  160. indent -= end;
  161. val = val.slice(0, -end);
  162. }
  163. }
  164. val += tail;
  165. t.value = val;
  166. t.selectionStart = t.selectionEnd = loc + indent + 1;
  167. } else if (e.key == 'Tab') {
  168. val = val.slice(0, loc) + ' ' + val.slice(t.selectionEnd);
  169. t.value = val;
  170. t.selectionStart = t.selectionEnd = loc + 2;
  171. } else if (t.selectionStart == t.selectionEnd) {
  172. if (e.key == 'Backspace') {
  173. if (val.charCodeAt(loc - 1) == 32 && !val.slice(val.lastIndexOf('\n', loc - 1), loc).trim().length) {
  174. val = val.slice(0, loc - 2) + val.slice(loc);
  175. t.value = val;
  176. t.selectionStart = t.selectionEnd = loc - 2;
  177. } else if (
  178. (val.charAt(loc - 1) == '{' && val.charAt(loc) == '}') ||
  179. (val.charAt(loc - 1) == '[' && val.charAt(loc) == ']') ||
  180. (val.charAt(loc - 1) == '(' && val.charAt(loc) == ')')
  181. ) {
  182. val = val.slice(0, loc - 1) + val.slice(loc + 1);
  183. t.value = val;
  184. t.selectionStart = t.selectionEnd = loc - 1;
  185. } else return;
  186. } else {
  187. let a: string;
  188. switch(e.key) {
  189. case '{':
  190. case '[':
  191. case '(':
  192. t.value = val = val.slice(0, loc) + (e.key == '{' ? '{}' : e.key == '[' ? '[]' : '()') + val.slice(loc);
  193. t.selectionStart = t.selectionEnd = newTmpParen = loc + 1;
  194. break;
  195. case '}':
  196. case ']':
  197. case ')':
  198. // BUG: if the cursor is moved, this false activates
  199. if (tmpParen.current != loc) {
  200. const lastNL = val.lastIndexOf('\n', loc - 1);
  201. const sl = val.slice(lastNL, loc);
  202. t.value = val = val.slice(0, loc - (sl.length > 1 && !sl.trim().length ? 2 : 0)) + e.key + val.slice(loc);
  203. }
  204. t.selectionEnd = t.selectionStart = loc + 1;
  205. break;
  206. default:
  207. tmpParen.current = -1;
  208. return;
  209. }
  210. };
  211. } else return;
  212. tmpParen.current = newTmpParen;
  213. e.preventDefault();
  214. onInput(val);
  215. }}
  216. onInput={e => onInput(e.currentTarget.value)}
  217. >
  218. {code}
  219. </textarea>
  220. </pre>
  221. </>
  222. )
  223. };
  224. const CodeBox: FC<{files: File[]}> = ({ files }) => {
  225. const [{ fflate, uzip, pako }, setCodes] = useState(presets['Streaming GZIP compression']);
  226. const onInput = (lib: 'fflate' | 'uzip' | 'pako', code: string) => {
  227. const codes: Preset = {
  228. fflate,
  229. uzip,
  230. pako
  231. };
  232. codes[lib] = code;
  233. setCodes(codes);
  234. }
  235. return (
  236. <div style={{
  237. display: 'flex',
  238. flexDirection: 'column',
  239. justifyContent: 'space-between',
  240. alignItems: 'center'
  241. }}>
  242. <div style={{
  243. display: 'flex',
  244. flexDirection: 'row',
  245. justifyContent: 'space-between',
  246. whiteSpace: 'pre-wrap',
  247. textAlign: 'left'
  248. }}>
  249. <div>
  250. fflate:
  251. <CodeHighlight code={fflate.trim()} onInput={t => onInput('fflate', t)} />
  252. </div>
  253. <div>
  254. UZIP (shimmed):
  255. <CodeHighlight code={uzip.trim()} onInput={t => onInput('uzip', t)} />
  256. </div>
  257. <div>
  258. Pako (shimmed):
  259. <CodeHighlight code={pako.trim()} onInput={t => onInput('pako', t)} />
  260. </div>
  261. </div>
  262. <button onClick={() => {
  263. let ts = Date.now();
  264. exec(fflate, files, out => {
  265. console.log('fflate took', Date.now() - ts);
  266. ts = Date.now();
  267. exec(uzip, files, out => {
  268. console.log('uzip took', Date.now() - ts);
  269. ts = Date.now();
  270. exec(pako, files, out => {
  271. console.log('pako took', Date.now() - ts);
  272. })
  273. })
  274. });
  275. }}>Run</button>
  276. </div>
  277. );
  278. }
  279. export default CodeBox;