Selaa lähdekoodia

Finished demo

Arjun Barrett 4 vuotta sitten
vanhempi
commit
4f5885d197

+ 2 - 1
docs/README.md

@@ -459,7 +459,8 @@ ___
 
 ▸ **unzipSync**(`data`: Uint8Array): [Unzipped](interfaces/unzipped.md)
 
-Synchronously decompresses a ZIP archive
+Synchronously decompresses a ZIP archive. Prefer using `unzip` for better
+performance with more than one file.
 
 #### Parameters:
 

+ 0 - 1
package.json

@@ -62,7 +62,6 @@
     "parcel": "^2.0.0-nightly.440",
     "parcel-config-precache-manifest": "^0.0.3",
     "preact": "^10.5.5",
-    "prism-react-renderer": "^1.1.1",
     "react": "^17.0.1",
     "react-dom": "^17.0.1",
     "rmwc": "^6.1.4",

+ 27 - 9
src/demo/App.tsx

@@ -1,9 +1,17 @@
-import React, { FC, useState } from 'react';
+import React, { FC, useEffect, useRef, useState } from 'react';
 import FilePicker from './components/file-picker';
 import CodeBox from './components/code-box';
 
 const App: FC = () => {
-  const [files, setFiles] = useState<File[]>([])
+  const [files, setFiles] = useState<File[] | null>([]);
+  const cbRef = useRef<HTMLDivElement>(null);
+  useEffect(() => {
+    if (files && files.length) {
+      cbRef.current!.scrollIntoView({
+        behavior: 'smooth' // Hopefully IE just ignores this value
+      });
+    }
+  }, [files]);
   return (
     <>
       <div style={{
@@ -40,7 +48,7 @@ const App: FC = () => {
         width: '100%',
         flex: 1
       }}>
-        <div style={{ maxWidth: '80%', fontSize: 'calc(18px + 0.6vw)', paddingTop: '4vh', paddingBottom: '2vh' }}>
+        <div style={{ maxWidth: '80%', fontSize: 'calc(15px + 0.6vw)', paddingTop: '4vh', paddingBottom: '2vh' }}>
           You've found <a href="//npmjs.com/package/fflate">fflate</a>, the fastest pure JavaScript compression library in existence.
           <br /><br />
           You can both pack and expand Zlib, GZIP, DEFLATE, or ZIP files very quickly with just a few lines of code.
@@ -49,13 +57,23 @@ const App: FC = () => {
           <br /><br />
           Despite utilizing multiple cores, supporting data streams, and being very memory efficient, fflate is compatible with both Node.js and browsers as old as IE11.
           <br /><br />
-          You can read more about fflate on <a href="//github.com/package/fflate">GitHub</a>. Try the demo below to see its performance for yourself.
-          <br />
-          <span style={{ fontSize: '0.5em' }}>I added a <i>lot</i> of sugar (around 4 hundred lines) to the UZIP and Pako APIs to make the demo clean and asynchronous, but the fflate API is unmodified.</span>
+          You can read more about fflate on <a href="//github.com/package/fflate">GitHub</a>. Try the demo below to see its performance for yourself. The code boxes are editable; try changing parameters or using a different compression format.
+          <br /><br />
+          <span style={{ color: 'gray' }}>Disclaimer: I added a <span style={{ fontStyle: 'italic' }}>lot</span> of sugar (around 4 hundred lines) to the UZIP and Pako APIs to make the demo clean and asynchronous, but the fflate API is unmodified.</span>
+          <br /><br />
         </div>
-        <div>
-          <FilePicker allowDirs onFiles={setFiles} onError={console.log} onDrag={() => {}} />
-          <CodeBox files={files} />
+        <div style={{
+          width: '100%',
+          display: 'flex',
+          alignItems: 'center',
+          flexDirection: 'column',
+          marginBottom: '2vh'
+        }}>
+          <FilePicker allowDirs onFiles={setFiles} onError={console.log} onDrag={() => {}}>
+            <div>{files ? ((files.length || 'No') + ' file' + (files.length == 1 ? '' : 's') + ' selected') : 'Loading...'}</div>
+            <br />
+          </FilePicker>
+          {((!files || files.length) && <CodeBox files={files!} forwardRef={cbRef} />) || null}
         </div>
        </div>
     </>

+ 327 - 108
src/demo/components/code-box/index.tsx

@@ -1,11 +1,15 @@
-import React, { FC, useRef, useState } from 'react';
-import Highlight, { DefaultProps } from 'prism-react-renderer';
-import Prism from './prism';
+import React, { FC, Ref, useEffect, useMemo, useRef, useState } from 'react';
+import { Prism } from './prism';
 import './prism';
 import './prism.css';
 import exec from './sandbox';
 
 const canStream = 'stream' in File.prototype;
+const rn = 'Running...';
+const wt = 'Waiting...';
+const tm = typeof performance != 'undefined'
+  ? () => performance.now()
+  : () => Date.now();
 
 type Preset = {
   fflate: string;
@@ -15,48 +19,187 @@ type Preset = {
 
 const presets: Record<string, Preset> = {
   'Basic GZIP compression': {
-    fflate: `var file = files[0];
-fileToU8(file, function(buf) {
-  fflate.gzip(buf, {
-    level: 6,
-    // These are optional, but fflate supports the metadata
-    mtime: file.lastModified,
-    filename: file.name
-  }, function(err, data) {
-    // Hope you're not causing any errors in the demo ;)
-    callback(data);
+    fflate: `var left = files.length;
+var filesLengths = {};
+// This function binds the variable "file" to the local scope, which makes
+// parallel processing possible.
+// If you use ES6, you can declare variables with "let" to automatically bind
+// the variable to the scope rather than using a separate function.
+var processFile = function(i) {
+  var file = files[i];
+  fileToU8(file, function(buf) {
+    fflate.gzip(buf, {
+      level: 6,
+
+      // These are optional, but fflate supports the metadata
+
+      mtime: file.lastModified,
+      filename: file.name
+    }, function(err, data) {
+      if (err) callback(err);
+      else {
+        filesLengths[file.name] = [data.length, file.size];
+    
+        // If you want to download the file to check it for yourself:
+        // download(data, 'myFile.gz');
+  
+        // If everyone else has finished processing already...
+        if (!--left) {
+          // Then return.
+          callback(prettySizes(filesLengths));
+        }
+      }
+    });
   });
-});`,
-    uzip: `var file = files[0];
-fileToU8(file, function(buf) {
-  // UZIP doesn't natively support GZIP, but I patched in support for it.
-  // In other words, you're better off using fflate for GZIP.
+}
+for (var i = 0; i < files.length; ++i) {
+  processFile(i);
+}`,
+    uzip: `var left = files.length;
+var filesLengths = {};
+var processFile = function(i) {
+  var file = files[i];
+  fileToU8(file, function(buf) {
+
+    // UZIP doesn't natively support GZIP, but I patched in support for it.
+    // In other words, you're better off using fflate for GZIP.
+  
+    // Also, UZIP runs synchronously on the main thread. It relies on global
+    // state, so you can't even run it in the background without causing bugs.
+  
+    // But just for the sake of a performance comparison, try it out.
+    uzipWorker.gzip(buf, function(err, data) {
+      if (err) callback(err);
+      else {
+        filesLengths[file.name] = [data.length, file.size];
+        if (!--left) callback(prettySizes(filesLengths));
+      }
+    });
+  });
+}
+for (var i = 0; i < files.length; ++i) {
+  processFile(i);
+}`,
+    pako: `var left = files.length;
+var filesLengths = {};
+var processFile = function(i) {
+  var file = files[i];
+  fileToU8(file, function(buf) {
 
-  // Also, UZIP runs synchronously on the main thread. It relies on global
-  // state, so you can't even run it in the background without causing bugs.
+    // Unlike UZIP, Pako natively supports GZIP, and it doesn't rely on global
+    // state. However, it's still 46kB for this basic functionality as opposed
+    // to fflate's 7kB, not to mention the fact that there's no easy way to use
+    // it asynchronously. I had to add a worker proxy for this to work.
 
-  // But just for the sake of a performance comparison, try it out.
-  uzipWorker.gzip(buf, function(err, data) {
-    callback(data);
+    pakoWorker.gzip(buf, function(err, data) {
+      if (err) callback(err)
+      else {
+        filesLengths[file.name] = [data.length, file.size];
+        if (!--left) callback(prettySizes(filesLengths));
+      }
+    });
   });
-});`,
-    pako: `var file = files[0];
-fileToU8(file, function(buf) {
-  // Unlike UZIP, Pako natively supports GZIP, and it doesn't rely on global
-  // state. However, it's still 46kB for this basic functionality as opposed
-  // to fflate's 7kB, not to mention the fact that there's no easy way to use
-  // it asynchronously. I had to add a worker proxy for this to work.
+}
+for (var i = 0; i < files.length; ++i) {
+  processFile(i);
+}`
+  },
+  'ZIP archive creation': {
+    fflate: `// fflate's ZIP API is asynchronous and parallelized (multithreaded)
+var left = files.length;
+var zipObj = {};
+var ALREADY_COMPRESSED = [
+  'zip', 'gz', 'png', 'jpg', 'jpeg', 'pdf', 'doc', 'docx', 'ppt', 'pptx',
+  'xls', 'xlsx', 'heic', 'heif', '7z', 'bz2', 'rar', 'gif', 'webp', 'webm'
+];
+
+// Yet again, this is necessary for parallelization.
+var processFile = function(i) {
+  var file = files[i];
+  var ext = file.name.slice(file.name.lastIndexOf('.') + 1);
+  fileToU8(file, function(buf) {
+    // With fflate, we can choose which files we want to compress
+    zipObj[file.name] = [buf, {
+      level: ALREADY_COMPRESSED.indexOf(ext) == -1 ? 6 : 0
+    }];
+    
+    // If we didn't want to specify options:
+    // zipObj[file.name] = buf;
+
+    if (!--left) {
+      fflate.zip(zipObj, {
+        // If you want to control options for every file, you can do so here
+        // They are merged with the per-file options (if they exist)
+        // mem: 9
+      }, function(err, out) {
+        if (err) callback(err);
+        else {
+          // You may want to try downloading to see that fflate actually works:
+          // download(out, 'fflate-demo.zip');
+          callback('Length ' + out.length);
+        }
+      });
+    }
+  });
+}
+for (var i = 0; i < files.length; ++i) {
+  processFile(i);
+}`,
+    uzip: `var left = files.length;
+var processFile = function(i) {
+  var file = files[i];
+  fileToU8(file, function(buf) {
+    // With UZIP, you cannot control the compression level of a file
+    // However, it skips compressing ZIP, JPEG, and PNG files out of the box.
+    zipObj.add(file.name, buf);
+    if (!--left) {
+      zipObj.ondata = function(err, out) {
+        if (err) callback(err);
+        else callback('Length ' + out.length);
+      }
+      zipObj.end();
+    }
+  });
+}
+// Reminder that this is custom sugar
+var zipObj = uzipWorker.zip();
+for (var i = 0; i < files.length; ++i) {
+  processFile(i);
+}`,
+    pako: `var left = files.length;
 
-  pakoWorker.gzip(buf, function(err, data) {
-    callback(data);
+// Internally, this uses JSZip. Despite its clean API, it suffers from
+// abysmal performance and awful compression ratios, particularly in v3.2.0
+// and up.
+// If you choose JSZip, make sure to use v3.1.5 for adequate performance
+// (2-3x slower than fflate) instead of the latest version, which is 20-30x
+// slower than fflate.
+var zipObj = pakoWorker.zip();
+var processFile = function(i) {
+  var file = files[i];
+  fileToU8(file, function(buf) {
+    // With JSZip, you cannot control the compression level of a file
+    zipObj.add(file.name, buf);
+    if (!--left) {
+      zipObj.ondata = function(err, out) {
+        if (err) callback(err);
+        else callback('Length ' + out.length);
+      }
+      zipObj.end();
+    }
   });
-}); `
+}
+for (var i = 0; i < files.length; ++i) {
+  processFile(i);
+}`
   }
 }
 
 if (canStream) {
   presets['Streaming GZIP compression'] = {
     fflate: `const { AsyncGzip } = fflate;
+// Theoretically, you could do this on every file, but I haven't done that here
+// for the sake of simplicity.
 const file = files[0];
 const gzipStream = new AsyncGzip({ level: 6 });
 // We can stream the file through GZIP to reduce memory usage
@@ -64,69 +207,85 @@ const fakeResponse = new Response(
   file.stream().pipeThrough(toNativeStream(gzipStream))
 );
 fakeResponse.arrayBuffer().then(buf => {
-  callback(new Uint8Array(buf));
+  callback('Length ' + buf.byteLength);
 });`,
-    uzip: `// UZIP doesn't support streaming to any extent`,
+    uzip: `// UZIP doesn't support streaming to any extent
+callback(new Error('unsupported'));`,
     pako: `// Hundreds of lines of code to make this run on a Worker...
 const file = files[0];
 // In case this wasn't clear already, Pako doesn't actually support this,
 // you need to create a custom async stream. I suppose you could copy the
-// code used in this demo.
+// code used in this demo, which is on GitHub under the src/demo directory.
 const gzipStream = pakoWorker.createGzip();
 const fakeResponse = new Response(
   file.stream().pipeThrough(toNativeStream(gzipStream))
 );
 fakeResponse.arrayBuffer().then(buf => {
-  callback(new Uint8Array(buf));
+  callback('Length ' + buf.byteLength);
 });`
   };
 }
 
+const availablePresets = Object.keys(presets);
+
 const CodeHighlight: FC<{
   code: string;
+  preset: string;
   onInput: (newCode: string) => void;
-}> = ({ code, onInput }) => {
-  const tmpParen = useRef(-1);
+}> = ({ code, preset, onInput }) => {
+  const highlight = useMemo(() => ({
+    __html: Prism.highlight(code + '\n', Prism.languages.javascript, 'javascript')
+  }), [code]);
+  const pre = useRef<HTMLPreElement>(null);
+  const ta = useRef<HTMLTextAreaElement>(null);
+  useEffect(() => {
+    pre.current!.addEventListener('scroll', () => {
+      ta.current!.scrollLeft = pre.current!.scrollLeft;
+      ta.current!.style.left = pre.current!.scrollLeft + 'px';
+    }, { passive: true });
+    ta.current!.addEventListener('scroll', () => {
+      pre.current!.scrollLeft = ta.current!.scrollLeft;
+    }, { passive: true });
+  }, []);
+  useEffect(() => {
+    ta.current!.value = code;
+  }, [preset]);
   return (
-    <pre className="language-javascript" style={{
-      width: '100%',
-      height: '100%',
+    <pre ref={pre} style={{
       position: 'relative',
       backgroundColor: '#2a2734',
       color: '#9a86fd',
-      fontSize: '0.7em'
+      maxWidth: 'calc(90vw - 2em)',
+      fontSize: '0.7em',
+      marginTop: '1em',
+      marginBottom: '1em',
+      padding: '1em',
+      overflow: 'auto',
+      fontFamily: 'Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace'
     }}>
-      <div>
-      <Highlight Prism={Prism.Prism as unknown as DefaultProps['Prism']} code={code} language="javascript">
-        {({ tokens, getLineProps, getTokenProps }) => (
-            tokens.map((line, i) => (
-              <div {...getLineProps({ line, key: i })}>
-                {line.map((token, key) => (
-                  token.empty ? <span style={{ display: 'inline-block' }} /> : <span {...getTokenProps({ token, key })} />
-                ))}
-              </div>
-            ))
-        )}
-      </Highlight>
-      </div>
+      <div dangerouslySetInnerHTML={highlight} />
       <textarea
+        ref={ta}
         autoComplete="off"
         autoCorrect="off"
         autoCapitalize="off"
         spellCheck="false"
         style={{
-          border: 'unset',
+          border: 0,
           resize: 'none',
           outline: 'none',
           position: 'absolute',
           background: 'transparent',
+          whiteSpace: 'pre',
           top: 0,
           left: 0,
-          width: '100%',
-          height: '100%',
+          width: 'calc(100% - 1em)',
+          height: 'calc(100% - 2em)',
+          overflow: 'hidden',
           lineHeight: 'inherit',
           fontSize: 'inherit',
           padding: 'inherit',
+          paddingRight: 0,
           color: 'transparent',
           caretColor: 'white',
           fontFamily: 'inherit'
@@ -135,7 +294,6 @@ const CodeHighlight: FC<{
           const t = e.currentTarget;
           let val = t.value;
           const loc = t.selectionStart;
-          let newTmpParen = -1;
           if (e.key == 'Enter') {
             const lastNL = val.lastIndexOf('\n', loc - 1);
             let indent = 0;
@@ -161,55 +319,53 @@ const CodeHighlight: FC<{
                 val = val.slice(0, -end);
               }
             }
-            val += tail;
-            t.value = val;
+            t.value = val += tail;
             t.selectionStart = t.selectionEnd = loc + indent + 1;
+            ta.current!.scrollLeft = 0;
           } else if (e.key == 'Tab') {
-            val = val.slice(0, loc) + '  ' + val.slice(t.selectionEnd);
-            t.value = val;
+            t.value = val = val.slice(0, loc) + '  ' + val.slice(t.selectionEnd);
             t.selectionStart = t.selectionEnd = loc + 2;
           } else if (t.selectionStart == t.selectionEnd) {
             if (e.key == 'Backspace') {
               if (val.charCodeAt(loc - 1) == 32 && !val.slice(val.lastIndexOf('\n', loc - 1), loc).trim().length) {
-                val = val.slice(0, loc - 2) + val.slice(loc);
-                t.value = val;
-                t.selectionStart = t.selectionEnd = loc - 2;
+                t.value = val.slice(0, loc - 1) + val.slice(loc);
+                t.selectionStart = t.selectionEnd = loc - 1;
               } else if (
                 (val.charAt(loc - 1) == '{' && val.charAt(loc) == '}') ||
                 (val.charAt(loc - 1) == '[' && val.charAt(loc) == ']') ||
                 (val.charAt(loc - 1) == '(' && val.charAt(loc) == ')')
               ) {
-                val = val.slice(0, loc - 1) + val.slice(loc + 1);
-                t.value = val;
-                t.selectionStart = t.selectionEnd = loc - 1;
-              } else return;
+                t.value = val.slice(0, loc) + val.slice(loc + 1);
+                // hack, doesn't work always
+                t.selectionStart = t.selectionEnd = loc;
+              }
+              return;
             } else {
-              let a: string;
               switch(e.key) {
                 case '{':
                 case '[':
                 case '(':
-                  t.value = val = val.slice(0, loc) + (e.key == '{' ? '{}' : e.key == '[' ? '[]' : '()') + val.slice(loc);
-                  t.selectionStart = t.selectionEnd = newTmpParen = loc + 1;
+                  t.value = val = val.slice(0, loc) + (e.key == '{' ? '}' : e.key == '[' ? ']' : ')') + val.slice(loc);
+                  t.selectionStart = t.selectionEnd = loc;
                   break;
                 case '}':
                 case ']':
-                case ')': 
+                case ')':
                   // BUG: if the cursor is moved, this false activates
-                  if (tmpParen.current != loc) {
+                  if (e.key == val.charAt(loc)) {
+                    t.value = val.slice(0, loc) + val.slice(loc + 1);
+                    t.selectionStart = t.selectionEnd = loc;
+                  } else {
                     const lastNL = val.lastIndexOf('\n', loc - 1);
                     const sl = val.slice(lastNL, loc);
-                    t.value = val = val.slice(0, loc - (sl.length > 1 && !sl.trim().length ? 2 : 0)) + e.key + val.slice(loc);
+                    const o = loc - (sl.length > 1 && !sl.trim().length ? 2 : 0);
+                    t.value = val.slice(0, o) + val.slice(loc);
+                    t.selectionStart = t.selectionEnd = o;
                   }
-                  t.selectionEnd = t.selectionStart = loc + 1;
-                  break;
-                default:
-                  tmpParen.current = -1;
-                  return;
               }
+              return;
             };
           } else return;
-          tmpParen.current = newTmpParen;
           e.preventDefault();
           onInput(val);
         }}
@@ -221,8 +377,19 @@ const CodeHighlight: FC<{
   )
 };
 
-const CodeBox: FC<{files: File[]}> = ({ files }) => {
-  const [{ fflate, uzip, pako }, setCodes] = useState(presets['Streaming GZIP compression']);
+const CodeBox: FC<{files: File[]; forwardRef: Ref<HTMLDivElement>}> = ({ files, forwardRef }) => {
+  const [preset, setPreset] = useState('Basic GZIP compression');
+  const [{ fflate, uzip, pako }, setCodes] = useState(presets[preset]);
+  const [ffl, setFFL] = useState('');
+  const [uz, setUZ] = useState('');
+  const [pk, setPK] = useState('');
+  useEffect(() => {
+    if (!files) {
+      setFFL('');
+      setUZ('');
+      setPK('');
+    }
+  }, [files]);
   const onInput = (lib: 'fflate' | 'uzip' | 'pako', code: string) => {
     const codes: Preset = {
       fflate,
@@ -231,50 +398,102 @@ const CodeBox: FC<{files: File[]}> = ({ files }) => {
     };
     codes[lib] = code;
     setCodes(codes);
+    setPreset('Custom');
   }
+  const [hover, setHover] = useState(false);
   return (
-    <div style={{
+    <div ref={forwardRef} style={{
       display: 'flex',
       flexDirection: 'column',
       justifyContent: 'space-between',
-      alignItems: 'center'
+      alignItems: 'center',
+      width: '100%',
+      flexWrap: 'wrap'
     }}>
+      <div>
+      <label>Preset: </label>
+        <select value={preset} onChange={e => {
+          let newPreset = e.currentTarget.value;
+          if (newPreset != 'Custom') setCodes(presets[newPreset]);
+          setPreset(newPreset);
+        }} style={{
+          marginTop: '2em'
+        }}>
+          {availablePresets.map(preset => <option key={preset} value={preset}>{preset}</option>)}
+          <option value="Custom">Custom</option>
+        </select>
+      </div>
       <div style={{
         display: 'flex',
         flexDirection: 'row',
-        justifyContent: 'space-between',
+        justifyContent: 'space-around',
         whiteSpace: 'pre-wrap',
-        textAlign: 'left'
+        textAlign: 'left',
+        flexWrap: 'wrap'
       }}>
-        <div>
+        <div style={{ padding: '2vmin' }}>
           fflate:
-          <CodeHighlight code={fflate} onInput={t => onInput('fflate', t)} />
-        </div>
-        <div>
-          UZIP (shimmed):
-          <CodeHighlight code={uzip} onInput={t => onInput('uzip', t)} />
+          <CodeHighlight code={fflate} preset={preset} onInput={t => onInput('fflate', t)} />
+          <span dangerouslySetInnerHTML={{ __html: ffl }} />
         </div>
-        <div>
-          Pako (shimmed):
-          <CodeHighlight code={pako} onInput={t => onInput('pako', t)} />
+        <div style={{
+          display: 'flex',
+          flexDirection: 'row',
+          flexWrap: 'wrap',
+          justifyContent: 'space-around',
+        }}>
+          <div style={{ padding: '2vmin' }}>
+            UZIP (shimmed):
+            <CodeHighlight code={uzip} preset={preset} onInput={t => onInput('uzip', t)} />
+            <span dangerouslySetInnerHTML={{ __html: uz }} />
+          </div>
+          <div style={{ padding: '2vmin' }}>
+            Pako (shimmed):
+            <CodeHighlight code={pako} preset={preset} onInput={t => onInput('pako', t)} />
+            <span dangerouslySetInnerHTML={{ __html: pk }} />
+          </div>
         </div>
       </div>
-      <button onClick={() => {
-        let ts = Date.now();
+      <button disabled={pk == 'Waiting...' || pk == 'Running...'} style={{
+        cursor: 'default',
+        width: '20vmin',
+        height: '6vh',
+        fontSize: '1.25em',
+        margin: '1vmin',
+        padding: '1vmin',
+        display: 'flex',
+        alignItems: 'center',
+        justifyContent: 'center',
+        boxShadow: '0 1px 2px 1px rgba(0, 0, 0, 0.2), 0 2px 4px 2px rgba(0, 0, 0, 0.15), 0 4px 8px 4px rgba(0, 0, 0, 0.12)',
+        border: '1px solid black',
+        borderRadius: '6px',
+        transition: 'background-color 300ms ease-in-out',
+        WebkitTouchCallout: 'none',
+        WebkitUserSelect: 'none',
+        msUserSelect: 'none',
+        MozUserSelect: 'none',
+        userSelect: 'none',
+        outline: 'none',
+        backgroundColor: hover ? 'rgba(0, 0, 0, 0.2)' : 'white'
+      }} onMouseOver={() => setHover(true)} onMouseLeave={() => setHover(false)} onClick={() => {
+        setHover(false);
+        const ts = tm();
+        setFFL(rn);
+        setUZ(wt);
+        setPK(wt);
         exec(fflate, files, out => {
-          console.log('fflate took', Date.now() - ts);
-          ts = Date.now();
+          const tf = tm();
+          setFFL('Finished in <span style="font-weight:bold">' + (tf - ts).toFixed(3) + 'ms</span>: ' + out);
           exec(uzip, files, out => {
-            console.log('uzip took', Date.now() - ts);
-            ts = Date.now();
+            const tu = tm();
+            setUZ('Finished in <span style="font-weight:bold">' + (tu - tf).toFixed(3) + 'ms:</span> ' + out);
             exec(pako, files, out => {
-              console.log('pako took', Date.now() - ts);
-            })
-          })
+              setPK('Finished in <span style="font-weight:bold">' + (tm() - tu).toFixed(3) + 'ms:</span> ' + out);
+            });
+          });
         });
       }}>Run</button>
     </div>
-    
   ); 
 }
 

+ 82 - 14
src/demo/components/code-box/sandbox.ts

@@ -3,14 +3,25 @@ import toNativeStream from './stream-adapter';
 
 type Callback = (...args: unknown[]) => void;
 type WorkerProxy = Record<string, Callback>;
+const concat = (chunks: Uint8Array[]) => {
+  const out = new Uint8Array(
+    chunks.reduce((a, v) => v.length + a, 0)
+  );
+  let loc = 0;
+  for (const chunk of chunks) {
+    out.set(chunk, loc);
+    loc += chunk.length;
+  }
+  return out;
+}
 const createWorkerProxy = (lib: string, keys: string[]): WorkerProxy => {
   const p: WorkerProxy = {};
   for (const k of keys) {
     const base = function(cb: (...args: unknown[]) => void) {
       const w = new Worker('../../util/workers.ts');
       w.postMessage([lib, k]);
-      w.onmessage = function() {
-        const args: unknown[] = Array.prototype.slice.call(arguments);
+      w.onmessage = function(msg) {
+        const args = msg.data;
         args.unshift(null);
         cb.apply(null, args);
       }
@@ -19,7 +30,16 @@ const createWorkerProxy = (lib: string, keys: string[]): WorkerProxy => {
     }
     if (k != 'zip' && k != 'unzip') {
       p[k] = function(dat, cb) {
-        const w = base(cb as Callback);
+        const chks: unknown[] = [];
+        const w = base((err, dat, final) => {
+          if (err) (cb as Callback)(err);
+          else {
+            if (final) {
+              if (!chks.length) (cb as Callback)(null, dat);
+              else (cb as Callback)(null, concat(chks as Uint8Array[]));
+            } else chks.push(dat);
+          }
+        });
         w.postMessage([dat, true], [(dat as Uint8Array).buffer]);
       }
       p['create' + k.slice(0, 1).toUpperCase() + k.slice(1)] = function() {
@@ -28,7 +48,7 @@ const createWorkerProxy = (lib: string, keys: string[]): WorkerProxy => {
           trueCb(err, dat, final);
         });
         const out = {
-          ondata: undefined,
+          ondata: trueCb,
           push(v: Uint8Array, f: boolean) {
             if (!out.ondata) throw 'no callback';
             trueCb = out.ondata;
@@ -42,12 +62,23 @@ const createWorkerProxy = (lib: string, keys: string[]): WorkerProxy => {
       }
     } else {
       p[k] = function() {
-        const w = base(arguments[arguments.length - 1]);
-        const bufs: ArrayBuffer[] = [];
-        for (const k in arguments[0]) {
-          bufs.push((arguments[k] = new Uint8Array(arguments[k])).buffer);
+        let trueCb = arguments[0];
+        const w = base((err, dat) => {
+          trueCb(err, dat);
+        });
+        const out = {
+          ondata: trueCb,
+          add(name: string, buf: Uint8Array) { 
+            buf = new Uint8Array(buf);
+            w.postMessage([name, buf], [buf.buffer]);
+          },
+          end() {
+            if (!out.ondata) throw 'no callback';
+            trueCb = out.ondata;
+            w.postMessage(null);
+          }
         }
-        w.postMessage(Array.prototype.slice.call(arguments, 0, -1), bufs);
+        return out;
       }
     }
   }
@@ -58,15 +89,46 @@ const keys = ['zip', 'unzip', 'deflate', 'inflate', 'gzip', 'gunzip', 'zlib', 'u
 
 const uzipWorker = createWorkerProxy('uzip', keys);
 const pakoWorker = createWorkerProxy('pako', keys);
-const fr = new FileReader();
 const fileToU8 = (file: File, cb: (out: Uint8Array) => void) => {
-  fr.onload = () => {
+  const fr = new FileReader();
+  fr.onloadend = () => {
     cb(new Uint8Array(fr.result as ArrayBuffer));
   }
   fr.readAsArrayBuffer(file);
 };
 
-const exec = (code: string, files: File[], callback: Callback): unknown => {
+const download = (file: BlobPart, name?: string) => {
+  const url = URL.createObjectURL(new Blob([file]));
+  const dl = document.createElement('a');
+  dl.download = name || ('fflate-demo-' + Date.now() + '.dat');
+  dl.href = url;
+  dl.click();
+  URL.revokeObjectURL(url);
+}
+
+const bts = ['B', ' kB', ' MB', ' GB'];
+
+const hrbt = (bt: number) => {
+  let i = 0;
+  for (; bt > 1023; ++i) bt /= 1024;
+  return bt.toFixed((i != 0) as unknown as number) + bts[i];
+}
+
+const prettySizes = (files: Record<string, [number, number]>) => {
+  let out = '\n\n';
+  let tot = 0;
+  let totc = 0;
+  let cnt = 0;
+  for (const k in files) {
+    ++cnt;
+    out += '<span style="font-weight:bold">' + k + '</span> compressed from <span style="font-weight:bold;color:red">' + hrbt(files[k][1]) + '</span> to <span style="font-weight:bold;color:green">' + hrbt(files[k][0]) + '</span>\n';
+    totc += files[k][0];
+    tot += files[k][1];
+  }
+  return out + (cnt > 1 ? '\n\n<span style="font-weight:bold">In total, all files originally <span style="font-style:italic;color:red">' + hrbt(tot) + '</span>, compressed to <span style="font-style:italic;color:green">' + hrbt(totc) + '</span></span>' : '');
+}
+
+const exec = (code: string, files: File[], callback: Callback) => {
   const scope = {
     fflate,
     uzipWorker,
@@ -74,9 +136,15 @@ const exec = (code: string, files: File[], callback: Callback): unknown => {
     toNativeStream,
     callback,
     fileToU8,
-    files
+    files,
+    download,
+    prettySizes
   };
-  return new Function('"use strict";' + Object.keys(scope).map(k => 'var ' + k + ' = this["' + k + '"];').join('') + code).call(scope);
+  try {
+    new Function('"use strict";' + Object.keys(scope).map(k => 'var ' + k + ' = this["' + k + '"];').join('') + code).call(scope);
+  } catch(e) {
+    callback(e);
+  }
 }
 
 export default exec;

+ 73 - 26
src/demo/components/file-picker/index.tsx

@@ -1,4 +1,4 @@
-import React, { CSSProperties, FC, HTMLAttributes, InputHTMLAttributes, useEffect, useRef } from 'react';
+import React, { CSSProperties, FC, HTMLAttributes, InputHTMLAttributes, useEffect, useRef, useState } from 'react';
 
 const supportsInputDirs = 'webkitdirectory' in HTMLInputElement.prototype;
 const supportsRelativePath = 'webkitRelativePath' in File.prototype;
@@ -37,7 +37,7 @@ const readRecurse = (dir: FileSystemDirectoryEntry, onComplete: (files: File[])
 }
 
 const FilePicker: FC<{
-  onFiles(files: File[]): void;
+  onFiles(files: File[] | null): void;
   onDrag(on: boolean): void;
   onError(err: string | Error): void;
   allowDirs: boolean;
@@ -45,6 +45,10 @@ const FilePicker: FC<{
 > = ({ onFiles, onDrag, onError, style, allowDirs, children, ...props }) => {
   const inputRef = useRef<HTMLInputElement>(null);
   const dirInputRef = useRef<HTMLInputElement>(null);
+  const dragRef = useRef(0);
+  const [inputHover, setInputHover] = useState(false);
+  const [dirInputHover, setDirInputHover] = useState(false);
+  const [isHovering, setIsHovering] = useState(false);
   useEffect(() => {
     // only init'd when support dirs
     if (dirInputRef.current) {
@@ -57,6 +61,7 @@ const FilePicker: FC<{
       const tf = ev.dataTransfer;
       if (!tf.files.length) onError('Please drop some files in');
       else {
+        onFiles(null);
         if (supportsDirs && allowDirs) {
           let outFiles: File[] = [];
           let lft = tf.items.length;
@@ -78,21 +83,26 @@ const FilePicker: FC<{
           }
         } else onFiles(Array.prototype.slice.call(tf.files));
       }
+      setIsHovering(false);
     },
     onDragEnter() {
+      ++dragRef.current;
       onDrag(true);
+      setIsHovering(true);
     },
     onDragOver(ev) {
       ev.preventDefault();
     },
-    onDragExit() {
-      onDrag(false);
+    onDragLeave() {
+      if (!--dragRef.current) {
+        onDrag(false);  
+        setIsHovering(false);
+      }
     },
     style: {
       display: 'flex',
-      flexDirection: 'row',
+      flexDirection: 'column',
       alignItems: 'center',
-      justifyContent: 'spac',
       ...style
     }
   };
@@ -103,7 +113,7 @@ const FilePicker: FC<{
         const outFiles: File[] = Array(files.length);
         for (let i = 0; i < files.length; ++i) {
           const file = files[i];
-          outFiles[i] = new File([file], file.webkitRelativePath, file);
+          outFiles[i] = new File([file], file.webkitRelativePath || file.name, file);
         }
         onFiles(outFiles);
       } else onFiles(Array.prototype.slice.call(files));
@@ -113,29 +123,66 @@ const FilePicker: FC<{
     multiple: true
   };
   const buttonStyles: CSSProperties = {
-    cursor: 'grab'
+    cursor: 'default',
+    minWidth: '8vw',
+    height: '6vh',
+    margin: '1vmin',
+    padding: '1vmin',
+    display: 'flex',
+    alignItems: 'center',
+    justifyContent: 'center',
+    boxShadow: '0 1px 2px 1px rgba(0, 0, 0, 0.2), 0 2px 4px 2px rgba(0, 0, 0, 0.15), 0 4px 8px 4px rgba(0, 0, 0, 0.12)',
+    border: '1px solid black',
+    borderRadius: '6px',
+    transition: 'background-color 300ms ease-in-out',
+    WebkitTouchCallout: 'none',
+    WebkitUserSelect: 'none',
+    msUserSelect: 'none',
+    MozUserSelect: 'none',
+    userSelect: 'none'
   };
   return (
     <div {...props} {...rootProps}>
       {children}
-      <input type="file" ref={inputRef} {...inputProps} />
-      <div onClick={() => inputRef.current!.click()} style={buttonStyles}>Files</div>
-      {supportsInputDirs && allowDirs &&
-        <>
-          <div style={{
-            borderLeft: '1px solid gray',
-            color: 'gray',
-            marginLeft: '2vw',
-            height: '100%',
-            display: 'flex',
-            alignItems: 'center'
-          }}>
-            <span style={{ position: 'relative', left: '-1vw', background: 'white' }}>OR</span>
-          </div>
-          <input type="file" ref={dirInputRef} {...inputProps} />
-          <div onClick={() => dirInputRef.current!.click()} style={buttonStyles}>Folders</div>
-        </>
-      }
+      <div style={{
+        transition: 'transform ' + (isHovering ? 300 : 50) + 'ms ease-in-out',
+        transform: isHovering ? 'scale(1.5)' : 'none'
+      }}>Drag and Drop</div>
+      <div style={{
+        borderBottom: '1px solid gray',
+        margin: '1.5vh',
+        color: 'gray',
+        lineHeight: 0,
+        paddingTop: '1.5vh',
+        marginBottom: '3vh',
+        width: '100%',
+      }}>
+        <span style={{ background: 'white', padding: '0.25em' }}>OR</span>
+      </div>
+      <div style={{
+        display: 'flex',
+        flexDirection: 'row',
+        alignItems: 'center',
+        justifyContent: 'center',
+        width: '100%'
+      }}>
+        <input type="file" ref={inputRef} {...inputProps} />
+        <div onClick={() => inputRef.current!.click()} onMouseOver={() => setInputHover(true)} onMouseOut={() => setInputHover(false)} style={{
+          ...buttonStyles,
+          backgroundColor: inputHover ? 'rgba(0, 0, 0, 0.14)' : 'white'
+        }}>Select Files</div>
+        {supportsInputDirs && allowDirs &&
+          <>
+            <div style={{ boxShadow: '1px 0 black', height: '100%' }}><span /></div>
+            <input type="file" ref={dirInputRef} {...inputProps} />
+            <div onClick={() => dirInputRef.current!.click()} onMouseOver={() => setDirInputHover(true)} onMouseOut={() => setDirInputHover(false)} style={{
+              ...buttonStyles,
+              marginLeft: '8vmin',
+              backgroundColor: dirInputHover ? 'rgba(0, 0, 0, 0.14)' : 'white'
+            }}>Select Folders</div>
+          </>
+        }
+      </div>
     </div>
   );
 }

+ 3 - 2
src/demo/index.css

@@ -2,9 +2,10 @@ html, body {
   margin: 0;
   padding: 0;
   font-family: Arial, Helvetica, sans-serif;
+  overflow-x: hidden;
 }
 
 #app {
-  height: 100vh;
-  width: 100vw;
+  min-height: 100vh;
+  overflow: hidden;
 }

+ 10 - 6
src/demo/util/workers.ts

@@ -59,7 +59,7 @@ onmessage = (ev: MessageEvent<[string, string]>) => {
           type: 'uint8array',
           compressionOptions: { level: 6 }
         }).then(buf => {
-          wk.postMessage(buf, [buf.buffer]);
+          wk.postMessage([buf, true], [buf.buffer]);
         })
       };
     } else if (type == 'unzip') {
@@ -75,7 +75,7 @@ onmessage = (ev: MessageEvent<[string, string]>) => {
             }));
           }
           Promise.all(bufs).then(res => {
-            wk.postMessage(out, res);
+            wk.postMessage([out, true], res);
           });
         })
       }
@@ -89,11 +89,14 @@ onmessage = (ev: MessageEvent<[string, string]>) => {
       ) : new pako.Inflate({
         raw: type == 'deflate'
       });
+      let chk: Uint8Array;
       strm.onData = (chunk: Uint8Array) => {
-        wk.postMessage(chunk, [chunk.buffer]);
+        if (chk) wk.postMessage([chk, false], [chk.buffer]);
+        chk = chunk;
       };
       onmessage = (ev: MessageEvent<[Uint8Array, boolean]>) => {
         strm.push(ev.data[0], ev.data[1]);
+        if (ev.data[1]) wk.postMessage([chk, true], [chk.buffer]);
       };
     }
   } else if (lib == 'uzip') {
@@ -103,8 +106,9 @@ onmessage = (ev: MessageEvent<[string, string]>) => {
         if (ev.data) {
           zip[ev.data[0]] = ev.data[1];
         } else {
+          console.log(zip);
           const buf = UZIP.encode(zip);
-          wk.postMessage(new Uint8Array(buf), [buf]);
+          wk.postMessage([new Uint8Array(buf), true], [buf]);
         }
       };
     } else if (type == 'unzip') {
@@ -115,7 +119,7 @@ onmessage = (ev: MessageEvent<[string, string]>) => {
           outBufs.push(bufs[k]);
           bufs[k] = new Uint8Array(bufs[k]);
         }
-        wk.postMessage(bufs, outBufs);
+        wk.postMessage([bufs, true], outBufs);
       }
     } else {
       const chunks: Uint8Array[] = [];
@@ -135,7 +139,7 @@ onmessage = (ev: MessageEvent<[string, string]>) => {
                     ? uzGzip(out)
                     // we can pray that there's no special header
                     : UZIP.inflateRaw(out.subarray(10, -8));
-          wk.postMessage(buf, [buf.buffer]);
+          wk.postMessage([buf, true], [buf.buffer]);
         }
       }
     }

+ 0 - 5
yarn.lock

@@ -6508,11 +6508,6 @@ prelude-ls@~1.1.2:
   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
   integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
 
-prism-react-renderer@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.1.1.tgz#1c1be61b1eb9446a146ca7a50b7bcf36f2a70a44"
-  integrity sha512-MgMhSdHuHymNRqD6KM3eGS0PNqgK9q4QF5P0yoQQvpB6jNjeSAi3jcSAz0Sua/t9fa4xDOMar9HJbLa08gl9ug==
-
 process-nextick-args@~2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"