diff options
author | Florian Dold <florian@dold.me> | 2021-08-20 13:31:03 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2021-08-20 13:31:03 +0200 |
commit | 859a8734c87d5e5081d7b0aa02cb538a7c801eac (patch) | |
tree | d2e9163479eb56ec382c842d62ce39fe30d450a4 /demo | |
download | node-vendor-859a8734c87d5e5081d7b0aa02cb538a7c801eac.tar.gz node-vendor-859a8734c87d5e5081d7b0aa02cb538a7c801eac.tar.bz2 node-vendor-859a8734c87d5e5081d7b0aa02cb538a7c801eac.zip |
Squashed 'fflate/' content from commit b786929
git-subtree-dir: fflate
git-subtree-split: b786929a368521e9cfcdcf7490a8e77485d5a253
Diffstat (limited to 'demo')
-rw-r--r-- | demo/App.tsx | 85 | ||||
-rw-r--r-- | demo/augment.d.ts | 45 | ||||
-rw-r--r-- | demo/components/code-box/index.tsx | 540 | ||||
-rw-r--r-- | demo/components/code-box/prism.css | 125 | ||||
-rw-r--r-- | demo/components/code-box/prism.js | 505 | ||||
-rw-r--r-- | demo/components/code-box/sandbox.ts | 150 | ||||
-rw-r--r-- | demo/components/code-box/stream-adapter.tsx | 17 | ||||
-rw-r--r-- | demo/components/file-picker/index.tsx | 190 | ||||
-rw-r--r-- | demo/favicon.ico | bin | 0 -> 5430 bytes | |||
-rw-r--r-- | demo/index.css | 11 | ||||
-rw-r--r-- | demo/index.html | 15 | ||||
-rw-r--r-- | demo/index.tsx | 11 | ||||
-rw-r--r-- | demo/sw.ts | 43 | ||||
-rw-r--r-- | demo/util/workers.ts | 146 |
14 files changed, 1883 insertions, 0 deletions
diff --git a/demo/App.tsx b/demo/App.tsx new file mode 100644 index 0000000..a6cfc6e --- /dev/null +++ b/demo/App.tsx @@ -0,0 +1,85 @@ +import React, { FC, useEffect, useRef, useState } from 'react'; +import FilePicker from './components/file-picker'; +import CodeBox from './components/code-box'; + +const App: FC = () => { + const [err, setErr] = useState<string | Error | null>(null); + 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={{ + display: 'flex', + fontSize: '70px', + justifyContent: 'space-between', + flexDirection: 'row', + overflow: 'hidden', + width: '100%', + fontWeight: 'bold' + }}> + <div style={{ paddingLeft: '0.25em' }}> + fflate + <div style={{ + color: 'gray', + fontSize: '0.25em', + fontWeight: 'lighter' + }}>a fast compression library by <a href="//github.com/101arrowz" style={{ color: 'gray' }}>101arrowz</a></div> + </div> + <a href="//github.com/101arrowz/fflate"> + <svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 250 250" fill="white"> + <path d="M0 0l115 115h15l12 27 108 108V0z" fill="black"/> + <path d="M128 109c-15-9-9-19-9-19 3-7 2-11 2-11-1-7 3-2 3-2 4 5 2 11 2 11-3 10 5 15 9 16" style={{ transformOrigin: '130px 106px' }} /> + <path d="M115 115s4 2 5 0l14-14c3-2 6-3 8-3-8-11-15-24 2-41 5-5 10-7 16-7 1-2 3-7 12-11 0 0 5 3 7 16 4 2 8 5 12 9s7 8 9 12c14 3 17 7 17 7-4 8-9 11-11 11 0 6-2 11-7 16-16 16-30 10-41 2 0 3-1 7-5 11l-12 11c-1 1 1 5 1 5z"/> + </svg> + </a> + </div> + <div style={{ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + flexDirection: 'column', + textAlign: 'center', + width: '100%', + flex: 1 + }}> + <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. + <br /><br /> + Weighing in at a measly 8kB for basic compression and decompression, you don't need to worry about your bundle size ballooning. + <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/101arrowz/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={{ fontSize: '0.75em' }}>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 style={{ + width: '100%', + display: 'flex', + alignItems: 'center', + flexDirection: 'column', + marginBottom: '2vh' + }}> + <FilePicker allowDirs onFiles={setFiles} onError={setErr} onDrag={() => {}}> + {err && <div style={{ color: 'red' }}>Error: {err}</div>} + <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> + </> + ); +} + +export default App;
\ No newline at end of file diff --git a/demo/augment.d.ts b/demo/augment.d.ts new file mode 100644 index 0000000..7f517ea --- /dev/null +++ b/demo/augment.d.ts @@ -0,0 +1,45 @@ +declare module 'uzip' { + namespace UZIP { + function deflateRaw(buf: Uint8Array, opts?: { level: number }): Uint8Array; + function inflateRaw(buf: Uint8Array, out?: Uint8Array): Uint8Array; + function deflate(buf: Uint8Array, opts?: { level: number }): Uint8Array; + function inflate(buf: Uint8Array, out?: Uint8Array): Uint8Array; + function encode(files: Record<string, Uint8Array>, noCmpr?: boolean): ArrayBuffer; + function parse(buf: ArrayBuffer): Record<string, ArrayBuffer>; + } + export = UZIP; +} + +interface DataTransferItem { + webkitGetAsEntry(): FileSystemEntry; +} + +interface BaseFileSystemEntry { + fullPath: string; + name: string; + isFile: boolean; + isDirectory: boolean; +} + +interface FileSystemFileEntry extends BaseFileSystemEntry { + isFile: true; + isDirectory: false + file(onSuccess: (file: File) => void, onError: (err: Error) => void): void; +} + +type FileSystemEntry = FileSystemFileEntry | FileSystemDirectoryEntry; + + +interface FileSystemDirectoryReader { + readEntries(onSuccess: (entries: FileSystemEntry[]) => void, onError: (err: Error) => void): void; +} + +interface FileSystemDirectoryEntry extends BaseFileSystemEntry { + isFile: false; + isDirectory: true; + createReader(): FileSystemDirectoryReader; +} + +interface File { + webkitRelativePath: string; +}
\ No newline at end of file diff --git a/demo/components/code-box/index.tsx b/demo/components/code-box/index.tsx new file mode 100644 index 0000000..e5b5779 --- /dev/null +++ b/demo/components/code-box/index.tsx @@ -0,0 +1,540 @@ +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; + uzip: string; + pako: string; +}; + +const presets: Record<string, Preset> = { + 'Basic GZIP compression': { + fflate: `var left = files.length; +var filesLengths = {}; + +// In a real app, use a list of file types to avoid compressing for better +// performance +var ALREADY_COMPRESSED = [ + 'zip', 'gz', 'png', 'jpg', 'jpeg', 'pdf', 'doc', 'docx', 'ppt', 'pptx', + 'xls', 'xlsx', 'heic', 'heif', '7z', 'bz2', 'rar', 'gif', 'webp', 'webm', + 'mp4', 'mov', 'mp3', 'aifc' +]; + +// 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, { + + // In a real app, instead of always compressing at a certain level, + // you'll want to check if the file is already compressed. For fairness, + // that's not done here. + + /* + level: ALREADY_COMPRESSED.indexOf( + file.name.slice(file.name.lastIndexOf('.') + 1).toLowerCase() + ) == -1 ? 6 : 0 + */ + + level: 6, + + // You can uncomment the below for a contest of pure algorithm speed. + // In a real app, you'll probably not need to set the memory level + // because fflate picks a reasonable level based on file size by default. + // If fflate performs worse than UZIP, you're probably passing in + // incompressible files; switching the level or the mem will fix it. + + /* + mem: 4 + */ + + // The following are optional, but fflate supports metadata if you want + 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)); + } + } + }); + }); +} +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) { + + // 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. + + pakoWorker.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); +}` + }, + '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', + 'mp4', 'mov', 'mp3', 'aifc' +]; + +// Yet again, this is necessary for parallelization. +var processFile = function(i) { + var file = files[i]; + var ext = file.name.slice(file.name.lastIndexOf('.') + 1).toLowerCase(); + 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; + +// 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 +const gzipped = file.stream().pipeThrough(toNativeStream(gzipStream)); +let gzSize = 0; +gzipped.pipeTo(new WritableStream({ + write(chunk) { + gzSize += chunk.length; + }, + close() { + callback('Length ' + gzSize); + } +}));`, + 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, which is on GitHub under the demo/ directory. +const gzipStream = pakoWorker.createGzip(); +const gzipped = file.stream().pipeThrough(toNativeStream(gzipStream)); +let gzSize = 0; +gzipped.pipeTo(new WritableStream({ + write(chunk) { + gzSize += chunk.length; + }, + close() { + callback('Length ' + gzSize); + } +}));` + }; +} + +const availablePresets = Object.keys(presets); + +const CodeHighlight: FC<{ + code: string; + preset: string; + onInput: (newCode: string) => void; +}> = ({ 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 ref={pre} style={{ + position: 'relative', + backgroundColor: '#2a2734', + color: '#9a86fd', + maxWidth: 'calc(90vw - 2em)', + fontSize: '0.7em', + marginTop: '1em', + marginBottom: '1em', + padding: '1em', + overflow: 'auto', + fontFamily: 'Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace' + }}> + <div dangerouslySetInnerHTML={highlight} /> + <textarea + ref={ta} + autoComplete="off" + autoCorrect="off" + autoCapitalize="off" + spellCheck="false" + style={{ + border: 0, + resize: 'none', + outline: 'none', + position: 'absolute', + background: 'transparent', + whiteSpace: 'pre', + top: 0, + left: 0, + width: 'calc(100% - 1em)', + height: 'calc(100% - 2em)', + overflow: 'hidden', + lineHeight: 'inherit', + fontSize: 'inherit', + padding: 'inherit', + paddingRight: 0, + color: 'transparent', + caretColor: 'white', + fontFamily: 'inherit' + }} + onKeyDown={e => { + const t = e.currentTarget; + let val = t.value; + const loc = t.selectionStart; + if (e.key == 'Enter') { + const lastNL = val.lastIndexOf('\n', loc - 1); + let indent = 0; + for (; val.charCodeAt(indent + lastNL + 1) == 32; ++indent); + const lastChar = val.charAt(loc - 1); + const nextChar = val.charAt(loc); + if (lastChar == '{'|| lastChar == '(' || lastChar == '[') indent += 2; + const addNL = nextChar == '}' || nextChar == ')' || nextChar == ']'; + const tail = val.slice(t.selectionEnd); + val = val.slice(0, loc) + '\n'; + for (let i = 0; i < indent; ++i) val += ' '; + if (addNL) { + if ( + (lastChar == '{' && nextChar == '}') || + (lastChar == '[' && nextChar == ']') || + (lastChar == '(' && nextChar == ')') + ) { + val += '\n'; + for (let i = 2; i < indent; ++i) val += ' '; + } else { + const end = Math.min(indent, 2); + indent -= end; + val = val.slice(0, -end); + } + } + t.value = val += tail; + t.selectionStart = t.selectionEnd = loc + indent + 1; + ta.current!.scrollLeft = 0; + } else if (e.key == 'Tab') { + 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) { + 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) == ')') + ) { + t.value = val.slice(0, loc) + val.slice(loc + 1); + // hack, doesn't work always + t.selectionStart = t.selectionEnd = loc; + } + return; + } else { + switch(e.key) { + case '{': + case '[': + case '(': + t.value = val = val.slice(0, loc) + (e.key == '{' ? '}' : e.key == '[' ? ']' : ')') + val.slice(loc); + t.selectionStart = t.selectionEnd = loc; + break; + case '}': + case ']': + case ')': + // BUG: if the cursor is moved, this false activates + 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); + 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; + } + } + return; + }; + } else return; + e.preventDefault(); + onInput(val); + }} + onInput={e => onInput(e.currentTarget.value)} + > + {code} + </textarea> + </pre> + ) +}; + +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, + uzip, + pako + }; + codes[lib] = code; + setCodes(codes); + setPreset('Custom'); + } + const [hover, setHover] = useState(false); + return ( + <div ref={forwardRef} style={{ + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + 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-around', + whiteSpace: 'pre-wrap', + textAlign: 'left', + flexWrap: 'wrap' + }}> + <div style={{ padding: '2vmin' }}> + fflate: + <CodeHighlight code={fflate} preset={preset} onInput={t => onInput('fflate', t)} /> + <span dangerouslySetInnerHTML={{ __html: ffl }} /> + </div> + <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 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 => { + const tf = tm(); + setFFL('Finished in <span style="font-weight:bold">' + (tf - ts).toFixed(3) + 'ms</span>: ' + out); + exec(uzip, files, out => { + const tu = tm(); + setUZ('Finished in <span style="font-weight:bold">' + (tu - tf).toFixed(3) + 'ms:</span> ' + out); + exec(pako, files, out => { + setPK('Finished in <span style="font-weight:bold">' + (tm() - tu).toFixed(3) + 'ms:</span> ' + out); + }); + }); + }); + }}>Run</button> + </div> + ); +} + +export default CodeBox;
\ No newline at end of file diff --git a/demo/components/code-box/prism.css b/demo/components/code-box/prism.css new file mode 100644 index 0000000..0bfe998 --- /dev/null +++ b/demo/components/code-box/prism.css @@ -0,0 +1,125 @@ +/* PrismJS 1.22.0 +https://prismjs.com/download.html#themes=prism-tomorrow&languages=clike+javascript */ +/** + * prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML + * Based on https://github.com/chriskempson/tomorrow-theme + * @author Rose Pritchard + */ + +code[class*="language-"], +pre[class*="language-"] { + color: #ccc; + background: none; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + font-size: 1em; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; + +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; +} + +:not(pre) > code[class*="language-"], +pre[class*="language-"] { + background: #2d2d2d; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + padding: .1em; + border-radius: .3em; + white-space: normal; +} + +.token.comment, +.token.block-comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: #999; +} + +.token.punctuation { + color: #ccc; +} + +.token.tag, +.token.attr-name, +.token.namespace, +.token.deleted { + color: #e2777a; +} + +.token.function-name { + color: #6196cc; +} + +.token.boolean, +.token.number, +.token.function { + color: #f08d49; +} + +.token.property, +.token.class-name, +.token.constant, +.token.symbol { + color: #f8c555; +} + +.token.selector, +.token.important, +.token.atrule, +.token.keyword, +.token.builtin { + color: #cc99cd; +} + +.token.string, +.token.char, +.token.attr-value, +.token.regex, +.token.variable { + color: #7ec699; +} + +.token.operator, +.token.entity, +.token.url { + color: #67cdcc; +} + +.token.important, +.token.bold { + font-weight: bold; +} +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} + +.token.inserted { + color: green; +} + diff --git a/demo/components/code-box/prism.js b/demo/components/code-box/prism.js new file mode 100644 index 0000000..e21620f --- /dev/null +++ b/demo/components/code-box/prism.js @@ -0,0 +1,505 @@ +/* PrismJS 1.22.0 +https://prismjs.com/download.html#themes=prism-tomorrow&languages=clike+javascript */ +var Prism = (function (u) { + var c = /\blang(?:uage)?-([\w-]+)\b/i, + n = 0, + M = { + manual: u.Prism && u.Prism.manual, + disableWorkerMessageHandler: + u.Prism && u.Prism.disableWorkerMessageHandler, + util: { + encode: function e(n) { + return n instanceof W + ? new W(n.type, e(n.content), n.alias) + : Array.isArray(n) + ? n.map(e) + : n + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/\u00a0/g, " "); + }, + type: function (e) { + return Object.prototype.toString.call(e).slice(8, -1); + }, + objId: function (e) { + return ( + e.__id || Object.defineProperty(e, "__id", { value: ++n }), e.__id + ); + }, + clone: function t(e, r) { + var a, n; + switch (((r = r || {}), M.util.type(e))) { + case "Object": + if (((n = M.util.objId(e)), r[n])) return r[n]; + for (var i in ((a = {}), (r[n] = a), e)) + e.hasOwnProperty(i) && (a[i] = t(e[i], r)); + return a; + case "Array": + return ( + (n = M.util.objId(e)), + r[n] + ? r[n] + : ((a = []), + (r[n] = a), + e.forEach(function (e, n) { + a[n] = t(e, r); + }), + a) + ); + default: + return e; + } + }, + getLanguage: function (e) { + for (; e && !c.test(e.className); ) e = e.parentElement; + return e + ? (e.className.match(c) || [, "none"])[1].toLowerCase() + : "none"; + }, + currentScript: function () { + if ("undefined" == typeof document) return null; + if ("currentScript" in document) return document.currentScript; + try { + throw new Error(); + } catch (e) { + var n = (/at [^(\r\n]*\((.*):.+:.+\)$/i.exec(e.stack) || [])[1]; + if (n) { + var t = document.getElementsByTagName("script"); + for (var r in t) if (t[r].src == n) return t[r]; + } + return null; + } + }, + isActive: function (e, n, t) { + for (var r = "no-" + n; e; ) { + var a = e.classList; + if (a.contains(n)) return !0; + if (a.contains(r)) return !1; + e = e.parentElement; + } + return !!t; + }, + }, + languages: { + extend: function (e, n) { + var t = M.util.clone(M.languages[e]); + for (var r in n) t[r] = n[r]; + return t; + }, + insertBefore: function (t, e, n, r) { + var a = (r = r || M.languages)[t], + i = {}; + for (var l in a) + if (a.hasOwnProperty(l)) { + if (l == e) for (var o in n) n.hasOwnProperty(o) && (i[o] = n[o]); + n.hasOwnProperty(l) || (i[l] = a[l]); + } + var s = r[t]; + return ( + (r[t] = i), + M.languages.DFS(M.languages, function (e, n) { + n === s && e != t && (this[e] = i); + }), + i + ); + }, + DFS: function e(n, t, r, a) { + a = a || {}; + var i = M.util.objId; + for (var l in n) + if (n.hasOwnProperty(l)) { + t.call(n, l, n[l], r || l); + var o = n[l], + s = M.util.type(o); + "Object" !== s || a[i(o)] + ? "Array" !== s || a[i(o)] || ((a[i(o)] = !0), e(o, t, l, a)) + : ((a[i(o)] = !0), e(o, t, null, a)); + } + }, + }, + plugins: {}, + highlightAll: function (e, n) { + M.highlightAllUnder(document, e, n); + }, + highlightAllUnder: function (e, n, t) { + var r = { + callback: t, + container: e, + selector: + 'code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code', + }; + M.hooks.run("before-highlightall", r), + (r.elements = Array.prototype.slice.apply( + r.container.querySelectorAll(r.selector) + )), + M.hooks.run("before-all-elements-highlight", r); + for (var a, i = 0; (a = r.elements[i++]); ) + M.highlightElement(a, !0 === n, r.callback); + }, + highlightElement: function (e, n, t) { + var r = M.util.getLanguage(e), + a = M.languages[r]; + e.className = + e.className.replace(c, "").replace(/\s+/g, " ") + " language-" + r; + var i = e.parentElement; + i && + "pre" === i.nodeName.toLowerCase() && + (i.className = + i.className.replace(c, "").replace(/\s+/g, " ") + " language-" + r); + var l = { element: e, language: r, grammar: a, code: e.textContent }; + function o(e) { + (l.highlightedCode = e), + M.hooks.run("before-insert", l), + (l.element.innerHTML = l.highlightedCode), + M.hooks.run("after-highlight", l), + M.hooks.run("complete", l), + t && t.call(l.element); + } + if ((M.hooks.run("before-sanity-check", l), !l.code)) + return M.hooks.run("complete", l), void (t && t.call(l.element)); + if ((M.hooks.run("before-highlight", l), l.grammar)) + if (n && u.Worker) { + var s = new Worker(M.filename); + (s.onmessage = function (e) { + o(e.data); + }), + s.postMessage( + JSON.stringify({ + language: l.language, + code: l.code, + immediateClose: !0, + }) + ); + } else o(M.highlight(l.code, l.grammar, l.language)); + else o(M.util.encode(l.code)); + }, + highlight: function (e, n, t) { + var r = { code: e, grammar: n, language: t }; + return ( + M.hooks.run("before-tokenize", r), + (r.tokens = M.tokenize(r.code, r.grammar)), + M.hooks.run("after-tokenize", r), + W.stringify(M.util.encode(r.tokens), r.language) + ); + }, + tokenize: function (e, n) { + var t = n.rest; + if (t) { + for (var r in t) n[r] = t[r]; + delete n.rest; + } + var a = new i(); + return ( + I(a, a.head, e), + (function e(n, t, r, a, i, l) { + for (var o in r) + if (r.hasOwnProperty(o) && r[o]) { + var s = r[o]; + s = Array.isArray(s) ? s : [s]; + for (var u = 0; u < s.length; ++u) { + if (l && l.cause == o + "," + u) return; + var c = s[u], + g = c.inside, + f = !!c.lookbehind, + h = !!c.greedy, + d = 0, + v = c.alias; + if (h && !c.pattern.global) { + var p = c.pattern.toString().match(/[imsuy]*$/)[0]; + c.pattern = RegExp(c.pattern.source, p + "g"); + } + for ( + var m = c.pattern || c, y = a.next, k = i; + y !== t.tail && !(l && k >= l.reach); + k += y.value.length, y = y.next + ) { + var b = y.value; + if (t.length > n.length) return; + if (!(b instanceof W)) { + var x = 1; + if (h && y != t.tail.prev) { + m.lastIndex = k; + var w = m.exec(n); + if (!w) break; + var A = w.index + (f && w[1] ? w[1].length : 0), + P = w.index + w[0].length, + S = k; + for (S += y.value.length; S <= A; ) + (y = y.next), (S += y.value.length); + if ( + ((S -= y.value.length), (k = S), y.value instanceof W) + ) + continue; + for ( + var E = y; + E !== t.tail && (S < P || "string" == typeof E.value); + E = E.next + ) + x++, (S += E.value.length); + x--, (b = n.slice(k, S)), (w.index -= k); + } else { + m.lastIndex = 0; + var w = m.exec(b); + } + if (w) { + f && (d = w[1] ? w[1].length : 0); + var A = w.index + d, + O = w[0].slice(d), + P = A + O.length, + L = b.slice(0, A), + N = b.slice(P), + j = k + b.length; + l && j > l.reach && (l.reach = j); + var C = y.prev; + L && ((C = I(t, C, L)), (k += L.length)), z(t, C, x); + var _ = new W(o, g ? M.tokenize(O, g) : O, v, O); + (y = I(t, C, _)), + N && I(t, y, N), + 1 < x && + e(n, t, r, y.prev, k, { + cause: o + "," + u, + reach: j, + }); + } + } + } + } + } + })(e, a, n, a.head, 0), + (function (e) { + var n = [], + t = e.head.next; + for (; t !== e.tail; ) n.push(t.value), (t = t.next); + return n; + })(a) + ); + }, + hooks: { + all: {}, + add: function (e, n) { + var t = M.hooks.all; + (t[e] = t[e] || []), t[e].push(n); + }, + run: function (e, n) { + var t = M.hooks.all[e]; + if (t && t.length) for (var r, a = 0; (r = t[a++]); ) r(n); + }, + }, + Token: W, + }; + function W(e, n, t, r) { + (this.type = e), + (this.content = n), + (this.alias = t), + (this.length = 0 | (r || "").length); + } + function i() { + var e = { value: null, prev: null, next: null }, + n = { value: null, prev: e, next: null }; + (e.next = n), (this.head = e), (this.tail = n), (this.length = 0); + } + function I(e, n, t) { + var r = n.next, + a = { value: t, prev: n, next: r }; + return (n.next = a), (r.prev = a), e.length++, a; + } + function z(e, n, t) { + for (var r = n.next, a = 0; a < t && r !== e.tail; a++) r = r.next; + ((n.next = r).prev = n), (e.length -= a); + } + if ( + ((u.Prism = M), + (W.stringify = function n(e, t) { + if ("string" == typeof e) return e; + if (Array.isArray(e)) { + var r = ""; + return ( + e.forEach(function (e) { + r += n(e, t); + }), + r + ); + } + var a = { + type: e.type, + content: n(e.content, t), + tag: "span", + classes: ["token", e.type], + attributes: {}, + language: t, + }, + i = e.alias; + i && + (Array.isArray(i) + ? Array.prototype.push.apply(a.classes, i) + : a.classes.push(i)), + M.hooks.run("wrap", a); + var l = ""; + for (var o in a.attributes) + l += + " " + + o + + '="' + + (a.attributes[o] || "").replace(/"/g, """) + + '"'; + return ( + "<" + + a.tag + + ' class="' + + a.classes.join(" ") + + '"' + + l + + ">" + + a.content + + "</" + + a.tag + + ">" + ); + }), + !u.document) + ) + return ( + u.addEventListener && + (M.disableWorkerMessageHandler || + u.addEventListener( + "message", + function (e) { + var n = JSON.parse(e.data), + t = n.language, + r = n.code, + a = n.immediateClose; + u.postMessage(M.highlight(r, M.languages[t], t)), a && u.close(); + }, + !1 + )), + M + ); + var e = M.util.currentScript(); + function t() { + M.manual || M.highlightAll(); + } + if ( + (e && + ((M.filename = e.src), e.hasAttribute("data-manual") && (M.manual = !0)), + !M.manual) + ) { + var r = document.readyState; + "loading" === r || ("interactive" === r && e && e.defer) + ? document.addEventListener("DOMContentLoaded", t) + : window.requestAnimationFrame + ? window.requestAnimationFrame(t) + : window.setTimeout(t, 16); + } + return M; +})(module.exports); +Prism.languages.clike = { + comment: [ + { pattern: /(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/, lookbehind: !0 }, + { pattern: /(^|[^\\:])\/\/.*/, lookbehind: !0, greedy: !0 }, + ], + string: { + pattern: /(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/, + greedy: !0, + }, + "class-name": { + pattern: /(\b(?:class|interface|extends|implements|trait|instanceof|new)\s+|\bcatch\s+\()[\w.\\]+/i, + lookbehind: !0, + inside: { punctuation: /[.\\]/ }, + }, + keyword: /\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/, + boolean: /\b(?:true|false)\b/, + function: /\w+(?=\()/, + number: /\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i, + operator: /[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/, + punctuation: /[{}[\];(),.:]/, +}; +(Prism.languages.javascript = Prism.languages.extend("clike", { + "class-name": [ + Prism.languages.clike["class-name"], + { + pattern: /(^|[^$\w\xA0-\uFFFF])[_$A-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\.(?:prototype|constructor))/, + lookbehind: !0, + }, + ], + keyword: [ + { pattern: /((?:^|})\s*)(?:catch|finally)\b/, lookbehind: !0 }, + { + pattern: /(^|[^.]|\.\.\.\s*)\b(?:as|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|for|from|function|(?:get|set)(?=\s*[\[$\w\xA0-\uFFFF])|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/, + lookbehind: !0, + }, + ], + number: /\b(?:(?:0[xX](?:[\dA-Fa-f](?:_[\dA-Fa-f])?)+|0[bB](?:[01](?:_[01])?)+|0[oO](?:[0-7](?:_[0-7])?)+)n?|(?:\d(?:_\d)?)+n|NaN|Infinity)\b|(?:\b(?:\d(?:_\d)?)+\.?(?:\d(?:_\d)?)*|\B\.(?:\d(?:_\d)?)+)(?:[Ee][+-]?(?:\d(?:_\d)?)+)?/, + function: /#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/, + operator: /--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/, +})), + (Prism.languages.javascript[ + "class-name" + ][0].pattern = /(\b(?:class|interface|extends|implements|instanceof|new)\s+)[\w.\\]+/), + Prism.languages.insertBefore("javascript", "keyword", { + regex: { + pattern: /((?:^|[^$\w\xA0-\uFFFF."'\])\s]|\b(?:return|yield))\s*)\/(?:\[(?:[^\]\\\r\n]|\\.)*]|\\.|[^/\\\[\r\n])+\/[gimyus]{0,6}(?=(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/, + lookbehind: !0, + greedy: !0, + inside: { + "regex-source": { + pattern: /^(\/)[\s\S]+(?=\/[a-z]*$)/, + lookbehind: !0, + alias: "language-regex", + inside: Prism.languages.regex, + }, + "regex-flags": /[a-z]+$/, + "regex-delimiter": /^\/|\/$/, + }, + }, + "function-variable": { + pattern: /#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)\s*=>))/, + alias: "function", + }, + parameter: [ + { + pattern: /(function(?:\s+[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)?\s*\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\))/, + lookbehind: !0, + inside: Prism.languages.javascript, + }, + { + pattern: /[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*=>)/i, + inside: Prism.languages.javascript, + }, + { + pattern: /(\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*=>)/, + lookbehind: !0, + inside: Prism.languages.javascript, + }, + { + pattern: /((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*\{)/, + lookbehind: !0, + inside: Prism.languages.javascript, + }, + ], + constant: /\b[A-Z](?:[A-Z_]|\dx?)*\b/, + }), + Prism.languages.insertBefore("javascript", "string", { + "template-string": { + pattern: /`(?:\\[\s\S]|\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}|(?!\${)[^\\`])*`/, + greedy: !0, + inside: { + "template-punctuation": { pattern: /^`|`$/, alias: "string" }, + interpolation: { + pattern: /((?:^|[^\\])(?:\\{2})*)\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}/, + lookbehind: !0, + inside: { + "interpolation-punctuation": { + pattern: /^\${|}$/, + alias: "punctuation", + }, + rest: Prism.languages.javascript, + }, + }, + string: /[\s\S]+/, + }, + }, + }), + Prism.languages.markup && + Prism.languages.markup.tag.addInlined("script", "javascript"), + (Prism.languages.js = Prism.languages.javascript); +module.exports.Prism = Prism;
\ No newline at end of file diff --git a/demo/components/code-box/sandbox.ts b/demo/components/code-box/sandbox.ts new file mode 100644 index 0000000..b03d2ce --- /dev/null +++ b/demo/components/code-box/sandbox.ts @@ -0,0 +1,150 @@ +import * as fflate from '../../..'; +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(msg) { + const args = msg.data; + args.unshift(null); + cb.apply(null, args); + } + w.onerror = err => cb(err); + return w; + } + if (k != 'zip' && k != 'unzip') { + p[k] = function(dat, cb) { + 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() { + let trueCb = arguments[0]; + const w = base((err, dat, final) => { + trueCb(err, dat, final); + }); + const out = { + ondata: trueCb, + push(v: Uint8Array, f: boolean) { + if (!out.ondata) throw 'no callback'; + trueCb = out.ondata; + w.postMessage([v, f], [v.buffer]); + }, + terminate() { + w.terminate(); + } + } + return out; + } + } else { + p[k] = function() { + 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); + } + } + return out; + } + } + } + return p; +} + +const keys = ['zip', 'unzip', 'deflate', 'inflate', 'gzip', 'gunzip', 'zlib', 'unzlib']; + +const uzipWorker = createWorkerProxy('uzip', keys); +const pakoWorker = createWorkerProxy('pako', keys); +const fileToU8 = (file: File, cb: (out: Uint8Array) => void) => { + const fr = new FileReader(); + fr.onloadend = () => { + cb(new Uint8Array(fr.result as ArrayBuffer)); + } + fr.readAsArrayBuffer(file); +}; + +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, + pakoWorker, + toNativeStream, + callback, + fileToU8, + files, + download, + prettySizes + }; + 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;
\ No newline at end of file diff --git a/demo/components/code-box/stream-adapter.tsx b/demo/components/code-box/stream-adapter.tsx new file mode 100644 index 0000000..a97d2ed --- /dev/null +++ b/demo/components/code-box/stream-adapter.tsx @@ -0,0 +1,17 @@ +import { AsyncDeflate } from '../../..'; +export default (stream: AsyncDeflate) => { + const writable = new WritableStream({ + write(dat: Uint8Array) { stream.push(dat); }, + close() { stream.push(new Uint8Array(0), true); } + }); + const readable = new ReadableStream({ + start(controller: ReadableStreamDefaultController<Uint8Array>) { + stream.ondata = (err, chunk, final) => { + if (err) writable.abort(err.message); + controller.enqueue(chunk); + if (final) controller.close(); + } + } + }); + return { readable, writable }; +}
\ No newline at end of file diff --git a/demo/components/file-picker/index.tsx b/demo/components/file-picker/index.tsx new file mode 100644 index 0000000..b88aad4 --- /dev/null +++ b/demo/components/file-picker/index.tsx @@ -0,0 +1,190 @@ +import React, { CSSProperties, FC, HTMLAttributes, InputHTMLAttributes, useEffect, useRef, useState } from 'react'; + +const supportsInputDirs = 'webkitdirectory' in HTMLInputElement.prototype; +const supportsRelativePath = 'webkitRelativePath' in File.prototype; +const supportsDirs = typeof DataTransferItem != 'undefined' && 'webkitGetAsEntry' in DataTransferItem.prototype; + +const readRecurse = (dir: FileSystemDirectoryEntry, onComplete: (files: File[]) => void, onError: (err: Error) => void) => { + let files: File[] = []; + let total = 0; + let errored = false; + let reachedEnd = false; + const onErr = (err: Error) => { + if (!errored) { + errored = true; + onError(err); + } + }; + const onDone = (f: File[]) => { + files = files.concat(f); + if (!--total && reachedEnd) onComplete(files); + }; + const reader = dir.createReader(); + const onRead = (entries: FileSystemEntry[]) => { + if (!entries.length && !errored) { + if (!total) onComplete(files); + else reachedEnd = true; + } else reader.readEntries(onRead, onError); + for (const entry of entries) { + ++total; + if (entry.isFile) entry.file(f => onDone([ + new File([f], entry.fullPath.slice(1), f) + ]), onErr); + else readRecurse(entry as FileSystemDirectoryEntry, onDone, onErr); + } + }; + reader.readEntries(onRead, onError); +} + +const FilePicker: FC<{ + onFiles(files: File[] | null): void; + onDrag(on: boolean): void; + onError(err: string | Error): void; + allowDirs: boolean; +} & Omit<HTMLAttributes<HTMLDivElement>, 'onError'> +> = ({ 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) { + dirInputRef.current.setAttribute('webkitdirectory', ''); + } + }, []); + const rootProps: HTMLAttributes<HTMLDivElement> = { + onDrop(ev) { + ev.preventDefault(); + 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; + let errored = false; + const onErr = (err: Error) => { + if (!errored) { + errored = true; + onError(err); + } + } + const onDone = (f: File[]) => { + outFiles = outFiles.concat(f); + if (!--lft && !errored) onFiles(outFiles); + }; + for (let i = 0; i < tf.items.length; ++i) { + const entry = tf.items[i].webkitGetAsEntry(); + if (entry.isFile) entry.file(f => onDone([f]), onErr); + else readRecurse(entry as FileSystemDirectoryEntry, onDone, onErr); + } + } else onFiles(Array.prototype.slice.call(tf.files)); + } + setIsHovering(false); + }, + onDragEnter() { + ++dragRef.current; + onDrag(true); + setIsHovering(true); + }, + onDragOver(ev) { + ev.preventDefault(); + }, + onDragLeave() { + if (!--dragRef.current) { + onDrag(false); + setIsHovering(false); + } + }, + style: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + ...style + } + }; + const inputProps: InputHTMLAttributes<HTMLInputElement> = { + onInput(ev) { + const t = ev.currentTarget, files = t.files!; + if (supportsRelativePath) { + 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.name, file); + } + onFiles(outFiles); + } else onFiles(Array.prototype.slice.call(files)); + t.value = ''; + }, + style: { display: 'none' }, + multiple: true + }; + const buttonStyles: CSSProperties = { + 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} + <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> + ); +} + +export default FilePicker;
\ No newline at end of file diff --git a/demo/favicon.ico b/demo/favicon.ico Binary files differnew file mode 100644 index 0000000..2daf032 --- /dev/null +++ b/demo/favicon.ico diff --git a/demo/index.css b/demo/index.css new file mode 100644 index 0000000..1334d7d --- /dev/null +++ b/demo/index.css @@ -0,0 +1,11 @@ +html, body { + margin: 0; + padding: 0; + font-family: Arial, Helvetica, sans-serif; + overflow-x: hidden; +} + +#app { + min-height: 100vh; + overflow: hidden; +}
\ No newline at end of file diff --git a/demo/index.html b/demo/index.html new file mode 100644 index 0000000..2a02b85 --- /dev/null +++ b/demo/index.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>fflate demo</title> + <link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+PHN2ZyB2aWV3Qm94PSIwIDAgMzIgMzIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmVyc2lvbj0iMS4xIj48c3R5bGU+cmVjdHtmaWxsOiMwMDB9QG1lZGlhKHByZWZlcnMtY29sb3Itc2NoZW1lOmRhcmspe3JlY3R7ZmlsbDojZmZmfX08L3N0eWxlPjxnIGlkPSJmIj48cmVjdCB4PSIyIiB5PSI2IiB3aWR0aD0iNCIgaGVpZ2h0PSIyMiIvPjxyZWN0IHg9IjAiIHk9IjEyIiB3aWR0aD0iMTIiIGhlaWdodD0iNCIgLz48cmVjdCB4PSI0IiB5PSI0IiB3aWR0aD0iNiIgaGVpZ2h0PSI0Ii8+PHJlY3QgeD0iNCIgeT0iNCIgd2lkdGg9IjYiIGhlaWdodD0iNCIvPjxyZWN0IHg9IjYiIHk9IjYiIHdpZHRoPSI2IiBoZWlnaHQ9IjIiLz48L2c+PHVzZSBocmVmPSIjZiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTQpIi8+PHJlY3QgeD0iMjgiIHk9IjQiIHdpZHRoPSI0IiBoZWlnaHQ9IjI0Ii8+PC9zdmc+"> + <link rel="icon" sizes="16x16" href="favicon.ico"> + <link rel="stylesheet" href="index.css"> +</head> +<body> + <div id="app"></div> + <script src="index.tsx"></script> +</body> +</html>
\ No newline at end of file diff --git a/demo/index.tsx b/demo/index.tsx new file mode 100644 index 0000000..b5ae88b --- /dev/null +++ b/demo/index.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import App from './App'; +import { render } from 'react-dom'; + +if (process.env.NODE_ENV == 'production') { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('sw.ts'); + } +} + +render(<App />, document.getElementById('app'));
\ No newline at end of file diff --git a/demo/sw.ts b/demo/sw.ts new file mode 100644 index 0000000..0cdbcde --- /dev/null +++ b/demo/sw.ts @@ -0,0 +1,43 @@ +/// <reference lib="webworker" /> + +const sw = self as unknown as ServiceWorkerGlobalScope & { + __precacheManifest: ({ url: string, revision: string })[]; +}; + +const precacheVersion = sw.__precacheManifest + .map(p => p.revision) + .join(''); +const precacheFiles = sw.__precacheManifest.map(p => p.url).filter( + u => /\.(ico)$/.test(u) +); + +const ch = () => caches.open(precacheVersion); + +sw.addEventListener('install', ev => { + // Do not finish installing until every file in the app has been cached + ev.waitUntil( + ch().then( + cache => cache.addAll(precacheFiles) + ) + ); +}); + +sw.addEventListener('activate', ev => { + ev.waitUntil( + caches.keys().then(keys => Promise.all( + keys.filter(k => k !== precacheVersion).map( + k => caches.delete(k) + ) + )).then(() => sw.clients.claim()) + ); +}); + +sw.addEventListener('fetch', ev => { + ev.respondWith( + caches.match(ev.request).then(resp => resp || ch().then(c => + fetch(ev.request).then(res => c.put(ev.request, res.clone()).then( + () => res + )) + )) + ) +});
\ No newline at end of file diff --git a/demo/util/workers.ts b/demo/util/workers.ts new file mode 100644 index 0000000..fa9d952 --- /dev/null +++ b/demo/util/workers.ts @@ -0,0 +1,146 @@ +import pako from 'pako'; +import * as UZIP from 'uzip'; +import JSZip from 'jszip'; + +const wk = self as unknown as { + postMessage(b: unknown, bufs: ArrayBuffer[]): void; +}; + +const dcmp = ['inflate', 'gunzip', 'unzlib']; + +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; +} + +// CRC32 table +const crct = new Uint32Array(256); +for (let i = 0; i < 256; ++i) { + let c = i, k = 9; + while (--k) c = ((c & 1) && 0xEDB88320) ^ (c >>> 1); + crct[i] = c; +} + +// CRC32 +const crc = (d: Uint8Array) => { + let c = 0xFFFFFFFF; + for (let i = 0; i < d.length; ++i) c = crct[(c & 255) ^ d[i]] ^ (c >>> 8); + return c ^ 0xFFFFFFFF; +} + +const uzGzip = (d: Uint8Array) => { + const raw = UZIP.deflateRaw(d); + const head = new Uint8Array([31, 139, 8, 0, 0, 0, 0, 0, 0, 0]); + const c = crc(d); + const l = raw.length; + const tail = new Uint8Array([ + c & 255, (c >>> 8) & 255, (c >>> 16) & 255, (c >>> 32) & 255, + l & 255, (l >>> 8) & 255, (l >>> 16) & 255, (l >>> 32) & 255, + ]); + return concat([head, raw, tail]); +} + +onmessage = (ev: MessageEvent<[string, string]>) => { + const [lib, type] = ev.data; + if (lib == 'pako') { + if (type == 'zip') { + const zip = new JSZip(); + onmessage = (ev: MessageEvent<null | [string, Uint8Array]>) => { + if (ev.data) { + zip.file(ev.data[0], ev.data[1]); + } else zip.generateAsync({ + type: 'uint8array', + compressionOptions: { level: 6 } + }).then(buf => { + wk.postMessage([buf, true], [buf.buffer]); + }) + }; + } else if (type == 'unzip') { + onmessage = (ev: MessageEvent<Uint8Array>) => { + JSZip.loadAsync(ev.data).then(zip => { + const out: Record<string, Uint8Array> = {}; + const bufs: Promise<ArrayBuffer>[] = []; + for (const k in zip.files) { + const file = zip.files[k]; + bufs.push(file.async('uint8array').then(v => { + out[file.name] = v; + return v.buffer; + })); + } + Promise.all(bufs).then(res => { + wk.postMessage([out, true], res); + }); + }) + } + } else { + const strm = dcmp.indexOf(type) == -1 + ? new pako.Deflate(type == 'gzip' ? { + gzip: true + } : { + raw: type == 'inflate' + } + ) : new pako.Inflate({ + raw: type == 'deflate' + }); + let chk: Uint8Array; + strm.onData = (chunk: Uint8Array) => { + 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') { + if (type == 'zip') { + const zip: Record<string, Uint8Array> = {}; + onmessage = (ev: MessageEvent<null | [string, Uint8Array]>) => { + if (ev.data) { + zip[ev.data[0]] = ev.data[1]; + } else { + const buf = UZIP.encode(zip); + wk.postMessage([new Uint8Array(buf), true], [buf]); + } + }; + } else if (type == 'unzip') { + onmessage = (ev: MessageEvent<Uint8Array>) => { + const bufs = UZIP.parse(ev.data.buffer); + const outBufs: ArrayBuffer[] = []; + for (const k in bufs) { + outBufs.push(bufs[k]); + bufs[k] = new Uint8Array(bufs[k]); + } + wk.postMessage([bufs, true], outBufs); + } + } else { + const chunks: Uint8Array[] = []; + onmessage = (ev: MessageEvent<[Uint8Array, boolean]>) => { + chunks.push(ev.data[0]); + if (ev.data[1]) { + const out = concat(chunks); + const buf = type == 'inflate' + ? UZIP.inflateRaw(out) + : type == 'deflate' + ? UZIP.deflateRaw(out) + : type == 'zlib' + ? UZIP.deflate(out) + : type == 'unzlib' + ? UZIP.inflate(out) + : type == 'gzip' + ? uzGzip(out) + // we can pray that there's no special header + : UZIP.inflateRaw(out.subarray(10, -8)); + wk.postMessage([buf, true], [buf.buffer]); + } + } + } + } +}
\ No newline at end of file |