diff options
Diffstat (limited to 'demo/components/code-box')
-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 |
5 files changed, 1337 insertions, 0 deletions
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 |