├── .datignore ├── LICENSE ├── README.md ├── dat.json ├── dev ├── env-default.js ├── theme-default.css ├── util.js ├── vendor │ ├── dynamic-import-polyfill.js │ ├── minimist-v1.2.0.js │ ├── nanohtml-v1.2.4.js │ └── nanomorph-v5.1.3.js ├── webterm.css └── webterm.js ├── favicon.ico └── index.html /.datignore: -------------------------------------------------------------------------------- 1 | .git 2 | .dat 3 | node_modules 4 | *.log 5 | **/.DS_Store 6 | Thumbs.db 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Paul Frazee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Webterm 2 | 3 | The Web Terminal provides power users with a familiar toolset for examining the system. For more background information, [read this blog post](http://pfrazee.github.io/blog/reimagining-the-browser-as-a-network-os). 4 | 5 | This is a work in progress. 6 | 7 | ## Todos 8 | 9 | - Create the program execution sandbox 10 | - Create the program RPC system 11 | - Add the user root archive 12 | - Load the terminal environment from files in the user root 13 | - Check that the destination exists before doing a CD 14 | - More builtin commands 15 | - History scrolling 16 | - Terminal autocomplete 17 | 18 | ## Objectives 19 | 20 | The "CLI" refers to the command-line interface (the UI and environment). 21 | 22 | - **The CLI SHOULD** support autocomplete, history, theming. 23 | - **The CLI SHOULD** employ a bashlike syntax which translates predictably to JS function invocations. 24 | - **The CLI SHOULD** use trusted UIs to confirm changes to the system. Password prompts should not be required. 25 | - **The CLI SHOULD NOT** allow the execution of untrusted Javascript in its context. 26 | 27 | "Commands" refer to the userspace programs which are executed by the CLI. 28 | 29 | - **Commands SHOULD** be executed in an isolated process, and communicate with the browser via IPC. 30 | - **Commands SHOULD** provide behaviors by interacting with the APIs provided by the browser. 31 | - **Commands SHOULD** be composable using sub-invocations. 32 | - **Commands SHOULD NOT** be able to invoke other commands. 33 | - **Commands SHOULD** render their output to the CLI using HTML. 34 | - **Commands SHOULD NOT** be able to provide custom JS or CSS for their HTML output. 35 | - **Commands SHOULD** be easily installable and managed (similar to `npm install -g {command}` but with better management). 36 | - **Commands SHOULD** be invocable from secure URLs, without prior installation (similar to `wget | bash`). 37 | - **Commands SHOULD** have a simple JS programming model which auto hydrates the execution environment and abstracts away all IPC. 38 | 39 | ### Explaining the objectives 40 | 41 | WebTerm is designed to be conservative in what it can express or compute. It should only invoke Javascript commands, which then execute against the browser's Web APIs. 42 | 43 | **We define elegance as simplicity, not power.** 44 | 45 | WebTerm's invocation syntax should be simple and obvious, and limit the number of edge-cases possible. Any "programming" should occur inside of a Javascript command. (The exact definition of "simple" will evolve as we understand the environment better. If conditionals or function definitions arrive in the shell language, then something has gone wrong.) 46 | 47 | ## Example commands 48 | 49 | Here is a minimal hello world example: 50 | 51 | ```js 52 | // hello-world.js 53 | function main () { 54 | return 'Hello, world!' 55 | } 56 | ``` 57 | 58 | Invocation: 59 | 60 | ```bash 61 | > hello-world 62 | Hello, world! 63 | ``` 64 | 65 | --- 66 | 67 | **Arguments** 68 | 69 | All command scripts define a main() which accepts an options object and positional arguments, and returns a JS value or object. Here's an example which echos the arguments: 70 | 71 | ```js 72 | // echo.js 73 | function main (opts, ...args) { 74 | return JSON.stringify(opts) + args.join(' ') 75 | } 76 | ``` 77 | 78 | Invocation: 79 | 80 | ```bash 81 | > echo -abc --hello world this is my echo 82 | {"a": true, "b": true, "c": true, "hello": "world"} this is my echo 83 | ``` 84 | 85 | --- 86 | 87 | **Globals** 88 | 89 | A `globals` object provides information about the environment. 90 | 91 | ```js 92 | // pwd.js 93 | function main () { 94 | return globals.cwd 95 | } 96 | ``` 97 | Invocation: 98 | ```bash 99 | > pwd 100 | dat://beakerbrowser.com/docs/ 101 | ``` 102 | 103 | --- 104 | 105 | **Rendering** 106 | 107 | The command may specify a `toHTML` function on the response object. This method will be invoked if/when the output is rendered to the cli. 108 | 109 | ```js 110 | // hello-big-world.js 111 | function main () { 112 | return { 113 | toHTML: () => '

HELLO WORLD!

' 114 | } 115 | } 116 | ``` 117 | 118 | The output can not include custom CSS. The CLI will provide a set of HTML constructs which it themes, and may even provided limited interactivity for exploring the output. 119 | 120 | --- 121 | 122 | **Sub-invocations** 123 | 124 | Commands can be composed by sub-invocations. Sub-invocations are command-invocations which are wrapped in parenthesis: `'(' invocation ')'`. They will be evaluated, and their value will be substituted in-place for their parent's invocation. 125 | 126 | ```js 127 | // change-case.js 128 | function main (opts, str) { 129 | str = str.toString() 130 | if (opts.u) return str.toUpperCase() 131 | if (opts.l) return str.toLowerCase() 132 | return str 133 | } 134 | ``` 135 | Invocation: 136 | ```bash 137 | > change-case -u (hello-world) 138 | HELLO, WORLD! 139 | # this is equivalent to running change-case -u "Hello, world!" 140 | ``` 141 | 142 | This can be used multiple times in an invocation, and nested arbitrarily: 143 | 144 | ```js 145 | // concat.js 146 | function main (opts, left, right) { 147 | return left.toString() + ' ' + right.toString() 148 | } 149 | ``` 150 | Invocation: 151 | ```bash 152 | > concat (hello-world) (hello-world) 153 | Hello, world! Hello, world! 154 | 155 | > concat (change-case -u (hello-world)) (change-case -l (hello-world)) 156 | HELLO, WORLD! hello, world! 157 | 158 | > change-case -u (change-case -l (change-case -u (hello-world))) 159 | HELLO, WORLD! 160 | ``` 161 | 162 | Sub-invocations are evaluated sequentially, to avoid potential races from side-effects (eg multiple sub-invocations using interactivity features). 163 | 164 | --- 165 | 166 | **Async** 167 | 168 | Commands may be async. 169 | 170 | ```js 171 | // wait1s.js 172 | async function main (opts) { 173 | await new Promise(resolve => setTimeout(resolve, 1e3)) 174 | } 175 | ``` 176 | 177 | --- 178 | 179 | **Interactivity** 180 | 181 | Commands may use the `terminal` API to provide interactivity during their invocation. 182 | 183 | ```js 184 | // new-profile.js 185 | async function main () { 186 | var name = await terminal.prompt('What is your name?') 187 | var bio = await terminal.prompt('What is your bio?') 188 | terminal.output(`You are ${name}. Bio: ${bio}`) 189 | var shouldPublish = await terminal.confirm('Publish?', true) 190 | if (shouldPublish) { 191 | await ... 192 | terminal.output('Published') 193 | } 194 | } 195 | ``` -------------------------------------------------------------------------------- /dat.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "webterm" 3 | } -------------------------------------------------------------------------------- /dev/env-default.js: -------------------------------------------------------------------------------- 1 | 2 | var config = { 3 | lsAfterCd: true 4 | } 5 | 6 | // interactive help 7 | // = 8 | 9 | const METHOD_HELP = [ 10 | {name: 'ls', description: 'List files in the directory'}, 11 | {name: 'cd', description: 'Change the current directory'}, 12 | {name: 'pwd', description: 'Fetch the current directory'}, 13 | {name: 'mkdir', description: 'Make a new directory'}, 14 | {name: 'rmdir', description: 'Remove an existing directory'}, 15 | {name: 'mv', description: 'Move a file or folder'}, 16 | {name: 'cp', description: 'Copy a file or folder'}, 17 | {name: 'rm', description: 'Remove a file'}, 18 | {name: 'echo', description: 'Output the arguments'} 19 | ] 20 | 21 | export function help () { 22 | return { 23 | toHTML() { 24 | var longestMethod = METHOD_HELP.reduce((acc, v) => Math.max(acc, v.name.length), 0) 25 | return METHOD_HELP.map(method => { 26 | var nSpaces = longestMethod + 2 - method.name.length 27 | var methodEl = env.html`${method.name}` 28 | methodEl.innerHTML += ' '.repeat(nSpaces) 29 | return env.html`
${methodEl} ${method.description || ''}
` 30 | }) 31 | } 32 | } 33 | } 34 | 35 | // current working directory methods 36 | // = 37 | 38 | export async function ls (opts = {}, location = '') { 39 | // pick target location 40 | const cwd = env.getCWD() 41 | location = toCWDLocation(location) 42 | // TODO add support for other domains than CWD 43 | 44 | // read 45 | var listing = await cwd.archive.readdir(location, {stat: true}) 46 | 47 | // render 48 | listing.toHTML = () => listing 49 | .filter(entry => { 50 | if (opts.all || opts.a) return true 51 | return entry.name.startsWith('.') === false 52 | }) 53 | .sort((a, b) => { 54 | // dirs on top 55 | if (a.stat.isDirectory() && !b.stat.isDirectory()) return -1 56 | if (!a.stat.isDirectory() && b.stat.isDirectory()) return 1 57 | return a.name.localeCompare(b.name) 58 | }) 59 | .map(entry => { 60 | // coloring 61 | var color = 'default' 62 | if (entry.name.startsWith('.')) { 63 | color = 'muted' 64 | } 65 | 66 | function onclick (e) { 67 | e.preventDefault() 68 | e.stopPropagation() 69 | env.evalCommand(`cd ${entry.name}`) 70 | } 71 | 72 | // render 73 | const entryUrl = cwd.archive.url + joinPath(location, entry.name) 74 | const tag = entry.stat.isDirectory() ? 'strong' : 'span' 75 | return env.html` 76 |
77 | <${tag}> 78 | ${entry.name} 83 | 84 |
` 85 | }) 86 | 87 | return listing 88 | } 89 | 90 | export async function cd (opts = {}, location = '') { 91 | location = location.toString() 92 | if (location === '~') { 93 | location = `dat://${window.location.hostname}` 94 | } 95 | if (location.startsWith('//')) { 96 | location = `dat://${location}` 97 | } else if (location.startsWith('/')) { 98 | location = `dat://${env.getCWD().host}${location}` 99 | } 100 | 101 | await env.setCWD(location) 102 | 103 | if (config.lsAfterCd) { 104 | return ls() 105 | } 106 | } 107 | 108 | export function pwd () { 109 | const cwd = env.getCWD() 110 | return `dat://${cwd.host}${cwd.pathname}` 111 | } 112 | 113 | // folder manipulation 114 | // = 115 | 116 | export async function mkdir (opts, dst) { 117 | if (!dst) throw new Error('dst is required') 118 | const cwd = env.getCWD() 119 | dst = toCWDLocation(dst) 120 | await cwd.archive.mkdir(dst) 121 | } 122 | 123 | export async function rmdir (opts, dst) { 124 | if (!dst) throw new Error('dst is required') 125 | const cwd = env.getCWD() 126 | dst = toCWDLocation(dst) 127 | var opts = {recursive: opts.r || opts.recursive} 128 | await cwd.archive.rmdir(dst, opts) 129 | } 130 | 131 | // file & folder manipulation 132 | // = 133 | 134 | export async function mv (opts, src, dst) { 135 | if (!src) throw new Error('src is required') 136 | if (!dst) throw new Error('dst is required') 137 | const cwd = env.getCWD() 138 | src = toCWDLocation(src) 139 | dst = toCWDLocation(dst) 140 | await cwd.archive.rename(src, dst) 141 | } 142 | 143 | export async function cp (opts, src, dst) { 144 | if (!src) throw new Error('src is required') 145 | if (!dst) throw new Error('dst is required') 146 | const cwd = env.getCWD() 147 | src = toCWDLocation(src) 148 | dst = toCWDLocation(dst) 149 | await cwd.archive.copy(src, dst) 150 | } 151 | 152 | // file manipulation 153 | // = 154 | 155 | export async function rm (opts, dst) { 156 | if (!dst) throw new Error('dst is required') 157 | const cwd = env.getCWD() 158 | dst = toCWDLocation(dst) 159 | await cwd.archive.unlink(dst) 160 | } 161 | 162 | // utilities 163 | // = 164 | 165 | export async function echo (opts, ...args) { 166 | var appendFlag = opts.a || opts.append 167 | var res = args.join(' ') 168 | const cwd = env.getCWD() 169 | 170 | if (opts.to) { 171 | let dst = toCWDLocation(opts.to) 172 | if (appendFlag) { 173 | let content = await cwd.archive.readFile(dst, 'utf8') 174 | res = content + res 175 | } 176 | await cwd.archive.writeFile(dst, res) 177 | } else { 178 | return res 179 | } 180 | } 181 | 182 | // internal methods 183 | // = 184 | 185 | function toCWDLocation (location) { 186 | const cwd = env.getCWD() 187 | location = location.toString() 188 | if (!location.startsWith('/')) { 189 | location = joinPath(cwd.pathname, location) 190 | } 191 | return location 192 | } 193 | 194 | function joinPath (left, right) { 195 | left = (left || '').toString() 196 | right = (right || '').toString() 197 | if (left.endsWith('/') && right.startsWith('/')) { 198 | return left + right.slice(1) 199 | } 200 | if (!left.endsWith('/') && !right.startsWith('/')) { 201 | return left + '/' + right 202 | } 203 | return left + right 204 | } -------------------------------------------------------------------------------- /dev/theme-default.css: -------------------------------------------------------------------------------- 1 | .text-default { 2 | color: inherit; 3 | } 4 | .text-muted { 5 | color: gray; 6 | } -------------------------------------------------------------------------------- /dev/util.js: -------------------------------------------------------------------------------- 1 | export function joinPath (left, right) { 2 | left = (left || '').toString() 3 | right = (right || '').toString() 4 | if (left.endsWith('/') && right.startsWith('/')) { 5 | return left + right.slice(1) 6 | } 7 | if (!left.endsWith('/') && !right.startsWith('/')) { 8 | return left + '/' + right 9 | } 10 | return left + right 11 | } -------------------------------------------------------------------------------- /dev/vendor/dynamic-import-polyfill.js: -------------------------------------------------------------------------------- 1 | // https://github.com/uupaa/dynamic-import-polyfill 2 | 3 | // MIT License 4 | 5 | // Copyright (c) 2018 uupaa 6 | 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | function toAbsoluteURL(url) { 26 | const a = document.createElement("a"); 27 | a.setAttribute("href", url); // 28 | return a.cloneNode(false).href; // -> "http://example.com/hoge.html" 29 | } 30 | 31 | export function importModule(url) { 32 | return new Promise((resolve, reject) => { 33 | const vector = "$importModule$" + Math.random().toString(32).slice(2); 34 | const script = document.createElement("script"); 35 | const destructor = () => { 36 | delete window[vector]; 37 | script.onerror = null; 38 | script.onload = null; 39 | script.remove(); 40 | URL.revokeObjectURL(script.src); 41 | script.src = ""; 42 | }; 43 | script.defer = "defer"; 44 | script.type = "module"; 45 | script.onerror = () => { 46 | reject(new Error(`Failed to import: ${url}`)); 47 | destructor(); 48 | }; 49 | script.onload = () => { 50 | resolve(window[vector]); 51 | destructor(); 52 | }; 53 | const absURL = toAbsoluteURL(url); 54 | const loader = `import * as m from "${absURL}"; window.${vector} = m;`; // export Module 55 | const blob = new Blob([loader], { type: "text/javascript" }); 56 | script.src = URL.createObjectURL(blob); 57 | 58 | document.head.appendChild(script); 59 | }); 60 | } 61 | 62 | export default importModule; 63 | -------------------------------------------------------------------------------- /dev/vendor/minimist-v1.2.0.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright James "Substack" Halliday 2018 3 | https://github.com/substack/minimist 4 | 5 | This software is released under the MIT license: 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of 8 | this software and associated documentation files (the "Software"), to deal in 9 | the Software without restriction, including without limitation the rights to 10 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 11 | the Software, and to permit persons to whom the Software is furnished to do so, 12 | subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 19 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 20 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 21 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 22 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | export default function (args, opts) { 26 | if (!opts) opts = {}; 27 | 28 | var flags = { bools : {}, strings : {}, unknownFn: null }; 29 | 30 | if (typeof opts['unknown'] === 'function') { 31 | flags.unknownFn = opts['unknown']; 32 | } 33 | 34 | if (typeof opts['boolean'] === 'boolean' && opts['boolean']) { 35 | flags.allBools = true; 36 | } else { 37 | [].concat(opts['boolean']).filter(Boolean).forEach(function (key) { 38 | flags.bools[key] = true; 39 | }); 40 | } 41 | 42 | var aliases = {}; 43 | Object.keys(opts.alias || {}).forEach(function (key) { 44 | aliases[key] = [].concat(opts.alias[key]); 45 | aliases[key].forEach(function (x) { 46 | aliases[x] = [key].concat(aliases[key].filter(function (y) { 47 | return x !== y; 48 | })); 49 | }); 50 | }); 51 | 52 | [].concat(opts.string).filter(Boolean).forEach(function (key) { 53 | flags.strings[key] = true; 54 | if (aliases[key]) { 55 | flags.strings[aliases[key]] = true; 56 | } 57 | }); 58 | 59 | var defaults = opts['default'] || {}; 60 | 61 | var argv = { _ : [] }; 62 | Object.keys(flags.bools).forEach(function (key) { 63 | setArg(key, defaults[key] === undefined ? false : defaults[key]); 64 | }); 65 | 66 | var notFlags = []; 67 | 68 | if (args.indexOf('--') !== -1) { 69 | notFlags = args.slice(args.indexOf('--')+1); 70 | args = args.slice(0, args.indexOf('--')); 71 | } 72 | 73 | function argDefined(key, arg) { 74 | return (flags.allBools && /^--[^=]+$/.test(arg)) || 75 | flags.strings[key] || flags.bools[key] || aliases[key]; 76 | } 77 | 78 | function setArg (key, val, arg) { 79 | if (arg && flags.unknownFn && !argDefined(key, arg)) { 80 | if (flags.unknownFn(arg) === false) return; 81 | } 82 | 83 | var value = !flags.strings[key] && isNumber(val) 84 | ? Number(val) : val 85 | ; 86 | setKey(argv, key.split('.'), value); 87 | 88 | (aliases[key] || []).forEach(function (x) { 89 | setKey(argv, x.split('.'), value); 90 | }); 91 | } 92 | 93 | function setKey (obj, keys, value) { 94 | var o = obj; 95 | keys.slice(0,-1).forEach(function (key) { 96 | if (o[key] === undefined) o[key] = {}; 97 | o = o[key]; 98 | }); 99 | 100 | var key = keys[keys.length - 1]; 101 | if (o[key] === undefined || flags.bools[key] || typeof o[key] === 'boolean') { 102 | o[key] = value; 103 | } 104 | else if (Array.isArray(o[key])) { 105 | o[key].push(value); 106 | } 107 | else { 108 | o[key] = [ o[key], value ]; 109 | } 110 | } 111 | 112 | function aliasIsBoolean(key) { 113 | return aliases[key].some(function (x) { 114 | return flags.bools[x]; 115 | }); 116 | } 117 | 118 | for (var i = 0; i < args.length; i++) { 119 | var arg = args[i]; 120 | 121 | if (/^--.+=/.test(arg)) { 122 | // Using [\s\S] instead of . because js doesn't support the 123 | // 'dotall' regex modifier. See: 124 | // http://stackoverflow.com/a/1068308/13216 125 | var m = arg.match(/^--([^=]+)=([\s\S]*)$/); 126 | var key = m[1]; 127 | var value = m[2]; 128 | if (flags.bools[key]) { 129 | value = value !== 'false'; 130 | } 131 | setArg(key, value, arg); 132 | } 133 | else if (/^--no-.+/.test(arg)) { 134 | var key = arg.match(/^--no-(.+)/)[1]; 135 | setArg(key, false, arg); 136 | } 137 | else if (/^--.+/.test(arg)) { 138 | var key = arg.match(/^--(.+)/)[1]; 139 | var next = args[i + 1]; 140 | if (next !== undefined && !/^-/.test(next) 141 | && !flags.bools[key] 142 | && !flags.allBools 143 | && (aliases[key] ? !aliasIsBoolean(key) : true)) { 144 | setArg(key, next, arg); 145 | i++; 146 | } 147 | else if (/^(true|false)$/.test(next)) { 148 | setArg(key, next === 'true', arg); 149 | i++; 150 | } 151 | else { 152 | setArg(key, flags.strings[key] ? '' : true, arg); 153 | } 154 | } 155 | else if (/^-[^-]+/.test(arg)) { 156 | var letters = arg.slice(1,-1).split(''); 157 | 158 | var broken = false; 159 | for (var j = 0; j < letters.length; j++) { 160 | var next = arg.slice(j+2); 161 | 162 | if (next === '-') { 163 | setArg(letters[j], next, arg) 164 | continue; 165 | } 166 | 167 | if (/[A-Za-z]/.test(letters[j]) && /=/.test(next)) { 168 | setArg(letters[j], next.split('=')[1], arg); 169 | broken = true; 170 | break; 171 | } 172 | 173 | if (/[A-Za-z]/.test(letters[j]) 174 | && /-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) { 175 | setArg(letters[j], next, arg); 176 | broken = true; 177 | break; 178 | } 179 | 180 | if (letters[j+1] && letters[j+1].match(/\W/)) { 181 | setArg(letters[j], arg.slice(j+2), arg); 182 | broken = true; 183 | break; 184 | } 185 | else { 186 | setArg(letters[j], flags.strings[letters[j]] ? '' : true, arg); 187 | } 188 | } 189 | 190 | var key = arg.slice(-1)[0]; 191 | if (!broken && key !== '-') { 192 | if (args[i+1] && !/^(-|--)[^-]/.test(args[i+1]) 193 | && !flags.bools[key] 194 | && (aliases[key] ? !aliasIsBoolean(key) : true)) { 195 | setArg(key, args[i+1], arg); 196 | i++; 197 | } 198 | else if (args[i+1] && /true|false/.test(args[i+1])) { 199 | setArg(key, args[i+1] === 'true', arg); 200 | i++; 201 | } 202 | else { 203 | setArg(key, flags.strings[key] ? '' : true, arg); 204 | } 205 | } 206 | } 207 | else { 208 | if (!flags.unknownFn || flags.unknownFn(arg) !== false) { 209 | argv._.push( 210 | flags.strings['_'] || !isNumber(arg) ? arg : Number(arg) 211 | ); 212 | } 213 | if (opts.stopEarly) { 214 | argv._.push.apply(argv._, args.slice(i + 1)); 215 | break; 216 | } 217 | } 218 | } 219 | 220 | Object.keys(defaults).forEach(function (key) { 221 | if (!hasKey(argv, key.split('.'))) { 222 | setKey(argv, key.split('.'), defaults[key]); 223 | 224 | (aliases[key] || []).forEach(function (x) { 225 | setKey(argv, x.split('.'), defaults[key]); 226 | }); 227 | } 228 | }); 229 | 230 | if (opts['--']) { 231 | argv['--'] = new Array(); 232 | notFlags.forEach(function(key) { 233 | argv['--'].push(key); 234 | }); 235 | } 236 | else { 237 | notFlags.forEach(function(key) { 238 | argv._.push(key); 239 | }); 240 | } 241 | 242 | return argv; 243 | }; 244 | 245 | function hasKey (obj, keys) { 246 | var o = obj; 247 | keys.slice(0,-1).forEach(function (key) { 248 | o = (o[key] || {}); 249 | }); 250 | 251 | var key = keys[keys.length - 1]; 252 | return key in o; 253 | } 254 | 255 | function isNumber (x) { 256 | if (typeof x === 'number') return true; 257 | if (/^0x[0-9a-f]+$/i.test(x)) return true; 258 | return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x); 259 | } 260 | -------------------------------------------------------------------------------- /dev/vendor/nanohtml-v1.2.4.js: -------------------------------------------------------------------------------- 1 | /* 2 | https://github.com/choojs/nanomorph 3 | 4 | Copyright 2018 Choo Contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | var global = {}; 14 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof global!=="undefined"){g=global}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.nanohtml = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 1) { 335 | stack.pop() 336 | stack[stack.length-1][0][2][ix] = h( 337 | cur[0], cur[1], cur[2].length ? cur[2] : undefined 338 | ) 339 | } 340 | } else if (s === OPEN) { 341 | var c = [p[1],{},[]] 342 | cur[2].push(c) 343 | stack.push([c,cur[2].length-1]) 344 | } else if (s === ATTR_KEY || (s === VAR && p[1] === ATTR_KEY)) { 345 | var key = '' 346 | var copyKey 347 | for (; i < parts.length; i++) { 348 | if (parts[i][0] === ATTR_KEY) { 349 | key = concat(key, parts[i][1]) 350 | } else if (parts[i][0] === VAR && parts[i][1] === ATTR_KEY) { 351 | if (typeof parts[i][2] === 'object' && !key) { 352 | for (copyKey in parts[i][2]) { 353 | if (parts[i][2].hasOwnProperty(copyKey) && !cur[1][copyKey]) { 354 | cur[1][copyKey] = parts[i][2][copyKey] 355 | } 356 | } 357 | } else { 358 | key = concat(key, parts[i][2]) 359 | } 360 | } else break 361 | } 362 | if (parts[i][0] === ATTR_EQ) i++ 363 | var j = i 364 | for (; i < parts.length; i++) { 365 | if (parts[i][0] === ATTR_VALUE || parts[i][0] === ATTR_KEY) { 366 | if (!cur[1][key]) cur[1][key] = strfn(parts[i][1]) 367 | else parts[i][1]==="" || (cur[1][key] = concat(cur[1][key], parts[i][1])); 368 | } else if (parts[i][0] === VAR 369 | && (parts[i][1] === ATTR_VALUE || parts[i][1] === ATTR_KEY)) { 370 | if (!cur[1][key]) cur[1][key] = strfn(parts[i][2]) 371 | else parts[i][2]==="" || (cur[1][key] = concat(cur[1][key], parts[i][2])); 372 | } else { 373 | if (key.length && !cur[1][key] && i === j 374 | && (parts[i][0] === CLOSE || parts[i][0] === ATTR_BREAK)) { 375 | // https://html.spec.whatwg.org/multipage/infrastructure.html#boolean-attributes 376 | // empty string is falsy, not well behaved value in browser 377 | cur[1][key] = key.toLowerCase() 378 | } 379 | if (parts[i][0] === CLOSE) { 380 | i-- 381 | } 382 | break 383 | } 384 | } 385 | } else if (s === ATTR_KEY) { 386 | cur[1][p[1]] = true 387 | } else if (s === VAR && p[1] === ATTR_KEY) { 388 | cur[1][p[2]] = true 389 | } else if (s === CLOSE) { 390 | if (selfClosing(cur[0]) && stack.length) { 391 | var ix = stack[stack.length-1][1] 392 | stack.pop() 393 | stack[stack.length-1][0][2][ix] = h( 394 | cur[0], cur[1], cur[2].length ? cur[2] : undefined 395 | ) 396 | } 397 | } else if (s === VAR && p[1] === TEXT) { 398 | if (p[2] === undefined || p[2] === null) p[2] = '' 399 | else if (!p[2]) p[2] = concat('', p[2]) 400 | if (Array.isArray(p[2][0])) { 401 | cur[2].push.apply(cur[2], p[2]) 402 | } else { 403 | cur[2].push(p[2]) 404 | } 405 | } else if (s === TEXT) { 406 | cur[2].push(p[1]) 407 | } else if (s === ATTR_EQ || s === ATTR_BREAK) { 408 | // no-op 409 | } else { 410 | throw new Error('unhandled: ' + s) 411 | } 412 | } 413 | 414 | if (tree[2].length > 1 && /^\s*$/.test(tree[2][0])) { 415 | tree[2].shift() 416 | } 417 | 418 | if (tree[2].length > 2 419 | || (tree[2].length === 2 && /\S/.test(tree[2][1]))) { 420 | throw new Error( 421 | 'multiple root elements must be wrapped in an enclosing tag' 422 | ) 423 | } 424 | if (Array.isArray(tree[2][0]) && typeof tree[2][0][0] === 'string' 425 | && Array.isArray(tree[2][0][2])) { 426 | tree[2][0] = h(tree[2][0][0], tree[2][0][1], tree[2][0][2]) 427 | } 428 | return tree[2][0] 429 | 430 | function parse (str) { 431 | var res = [] 432 | if (state === ATTR_VALUE_W) state = ATTR 433 | for (var i = 0; i < str.length; i++) { 434 | var c = str.charAt(i) 435 | if (state === TEXT && c === '<') { 436 | if (reg.length) res.push([TEXT, reg]) 437 | reg = '' 438 | state = OPEN 439 | } else if (c === '>' && !quot(state) && state !== COMMENT) { 440 | if (state === OPEN && reg.length) { 441 | res.push([OPEN,reg]) 442 | } else if (state === ATTR_KEY) { 443 | res.push([ATTR_KEY,reg]) 444 | } else if (state === ATTR_VALUE && reg.length) { 445 | res.push([ATTR_VALUE,reg]) 446 | } 447 | res.push([CLOSE]) 448 | reg = '' 449 | state = TEXT 450 | } else if (state === COMMENT && /-$/.test(reg) && c === '-') { 451 | if (opts.comments) { 452 | res.push([ATTR_VALUE,reg.substr(0, reg.length - 1)],[CLOSE]) 453 | } 454 | reg = '' 455 | state = TEXT 456 | } else if (state === OPEN && /^!--$/.test(reg)) { 457 | if (opts.comments) { 458 | res.push([OPEN, reg],[ATTR_KEY,'comment'],[ATTR_EQ]) 459 | } 460 | reg = c 461 | state = COMMENT 462 | } else if (state === TEXT || state === COMMENT) { 463 | reg += c 464 | } else if (state === OPEN && c === '/' && reg.length) { 465 | // no-op, self closing tag without a space
466 | } else if (state === OPEN && /\s/.test(c)) { 467 | if (reg.length) { 468 | res.push([OPEN, reg]) 469 | } 470 | reg = '' 471 | state = ATTR 472 | } else if (state === OPEN) { 473 | reg += c 474 | } else if (state === ATTR && /[^\s"'=/]/.test(c)) { 475 | state = ATTR_KEY 476 | reg = c 477 | } else if (state === ATTR && /\s/.test(c)) { 478 | if (reg.length) res.push([ATTR_KEY,reg]) 479 | res.push([ATTR_BREAK]) 480 | } else if (state === ATTR_KEY && /\s/.test(c)) { 481 | res.push([ATTR_KEY,reg]) 482 | reg = '' 483 | state = ATTR_KEY_W 484 | } else if (state === ATTR_KEY && c === '=') { 485 | res.push([ATTR_KEY,reg],[ATTR_EQ]) 486 | reg = '' 487 | state = ATTR_VALUE_W 488 | } else if (state === ATTR_KEY) { 489 | reg += c 490 | } else if ((state === ATTR_KEY_W || state === ATTR) && c === '=') { 491 | res.push([ATTR_EQ]) 492 | state = ATTR_VALUE_W 493 | } else if ((state === ATTR_KEY_W || state === ATTR) && !/\s/.test(c)) { 494 | res.push([ATTR_BREAK]) 495 | if (/[\w-]/.test(c)) { 496 | reg += c 497 | state = ATTR_KEY 498 | } else state = ATTR 499 | } else if (state === ATTR_VALUE_W && c === '"') { 500 | state = ATTR_VALUE_DQ 501 | } else if (state === ATTR_VALUE_W && c === "'") { 502 | state = ATTR_VALUE_SQ 503 | } else if (state === ATTR_VALUE_DQ && c === '"') { 504 | res.push([ATTR_VALUE,reg],[ATTR_BREAK]) 505 | reg = '' 506 | state = ATTR 507 | } else if (state === ATTR_VALUE_SQ && c === "'") { 508 | res.push([ATTR_VALUE,reg],[ATTR_BREAK]) 509 | reg = '' 510 | state = ATTR 511 | } else if (state === ATTR_VALUE_W && !/\s/.test(c)) { 512 | state = ATTR_VALUE 513 | i-- 514 | } else if (state === ATTR_VALUE && /\s/.test(c)) { 515 | res.push([ATTR_VALUE,reg],[ATTR_BREAK]) 516 | reg = '' 517 | state = ATTR 518 | } else if (state === ATTR_VALUE || state === ATTR_VALUE_SQ 519 | || state === ATTR_VALUE_DQ) { 520 | reg += c 521 | } 522 | } 523 | if (state === TEXT && reg.length) { 524 | res.push([TEXT,reg]) 525 | reg = '' 526 | } else if (state === ATTR_VALUE && reg.length) { 527 | res.push([ATTR_VALUE,reg]) 528 | reg = '' 529 | } else if (state === ATTR_VALUE_DQ && reg.length) { 530 | res.push([ATTR_VALUE,reg]) 531 | reg = '' 532 | } else if (state === ATTR_VALUE_SQ && reg.length) { 533 | res.push([ATTR_VALUE,reg]) 534 | reg = '' 535 | } else if (state === ATTR_KEY) { 536 | res.push([ATTR_KEY,reg]) 537 | reg = '' 538 | } 539 | return res 540 | } 541 | } 542 | 543 | function strfn (x) { 544 | if (typeof x === 'function') return x 545 | else if (typeof x === 'string') return x 546 | else if (x && typeof x === 'object') return x 547 | else return concat('', x) 548 | } 549 | } 550 | 551 | function quot (state) { 552 | return state === ATTR_VALUE_SQ || state === ATTR_VALUE_DQ 553 | } 554 | 555 | var hasOwn = Object.prototype.hasOwnProperty 556 | function has (obj, key) { return hasOwn.call(obj, key) } 557 | 558 | var closeRE = RegExp('^(' + [ 559 | 'area', 'base', 'basefont', 'bgsound', 'br', 'col', 'command', 'embed', 560 | 'frame', 'hr', 'img', 'input', 'isindex', 'keygen', 'link', 'meta', 'param', 561 | 'source', 'track', 'wbr', '!--', 562 | // SVG TAGS 563 | 'animate', 'animateTransform', 'circle', 'cursor', 'desc', 'ellipse', 564 | 'feBlend', 'feColorMatrix', 'feComposite', 565 | 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 566 | 'feDistantLight', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 567 | 'feGaussianBlur', 'feImage', 'feMergeNode', 'feMorphology', 568 | 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 569 | 'feTurbulence', 'font-face-format', 'font-face-name', 'font-face-uri', 570 | 'glyph', 'glyphRef', 'hkern', 'image', 'line', 'missing-glyph', 'mpath', 571 | 'path', 'polygon', 'polyline', 'rect', 'set', 'stop', 'tref', 'use', 'view', 572 | 'vkern' 573 | ].join('|') + ')(?:[\.#][a-zA-Z0-9\u007F-\uFFFF_:-]+)*$') 574 | function selfClosing (tag) { return closeRE.test(tag) } 575 | 576 | },{"hyperscript-attribute-to-property":6}]},{},[3])(3) 577 | }); 578 | var {nanohtml} = global 579 | export {nanohtml as default} -------------------------------------------------------------------------------- /dev/vendor/nanomorph-v5.1.3.js: -------------------------------------------------------------------------------- 1 | /* 2 | https://github.com/choojs/nanohtml 3 | 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 2016 Yoshua Wuyts 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | */ 26 | var global = {}; 27 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof global!=="undefined"){g=global}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.nanomorph = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o same: diff and walk children 40 | // -> not same: replace and return 41 | // old node doesn't exist 42 | // -> insert new node 43 | // new node doesn't exist 44 | // -> delete old node 45 | // nodes are not the same 46 | // -> diff nodes and apply patch to old node 47 | // nodes are the same 48 | // -> walk all child nodes and append to old node 49 | function nanomorph (oldTree, newTree) { 50 | // if (DEBUG) { 51 | // console.log( 52 | // 'nanomorph\nold\n %s\nnew\n %s', 53 | // oldTree && oldTree.outerHTML, 54 | // newTree && newTree.outerHTML 55 | // ) 56 | // } 57 | assert.equal(typeof oldTree, 'object', 'nanomorph: oldTree should be an object') 58 | assert.equal(typeof newTree, 'object', 'nanomorph: newTree should be an object') 59 | var tree = walk(newTree, oldTree) 60 | // if (DEBUG) console.log('=> morphed\n %s', tree.outerHTML) 61 | return tree 62 | } 63 | 64 | // Walk and morph a dom tree 65 | function walk (newNode, oldNode) { 66 | // if (DEBUG) { 67 | // console.log( 68 | // 'walk\nold\n %s\nnew\n %s', 69 | // oldNode && oldNode.outerHTML, 70 | // newNode && newNode.outerHTML 71 | // ) 72 | // } 73 | if (!oldNode) { 74 | return newNode 75 | } else if (!newNode) { 76 | return null 77 | } else if (newNode.isSameNode && newNode.isSameNode(oldNode)) { 78 | return oldNode 79 | } else if (newNode.tagName !== oldNode.tagName) { 80 | return newNode 81 | } else { 82 | morph(newNode, oldNode) 83 | updateChildren(newNode, oldNode) 84 | return oldNode 85 | } 86 | } 87 | 88 | // Update the children of elements 89 | // (obj, obj) -> null 90 | function updateChildren (newNode, oldNode) { 91 | // if (DEBUG) { 92 | // console.log( 93 | // 'updateChildren\nold\n %s\nnew\n %s', 94 | // oldNode && oldNode.outerHTML, 95 | // newNode && newNode.outerHTML 96 | // ) 97 | // } 98 | var oldChild, newChild, morphed, oldMatch 99 | 100 | // The offset is only ever increased, and used for [i - offset] in the loop 101 | var offset = 0 102 | 103 | for (var i = 0; ; i++) { 104 | oldChild = oldNode.childNodes[i] 105 | newChild = newNode.childNodes[i - offset] 106 | // if (DEBUG) { 107 | // console.log( 108 | // '===\n- old\n %s\n- new\n %s', 109 | // oldChild && oldChild.outerHTML, 110 | // newChild && newChild.outerHTML 111 | // ) 112 | // } 113 | // Both nodes are empty, do nothing 114 | if (!oldChild && !newChild) { 115 | break 116 | 117 | // There is no new child, remove old 118 | } else if (!newChild) { 119 | oldNode.removeChild(oldChild) 120 | i-- 121 | 122 | // There is no old child, add new 123 | } else if (!oldChild) { 124 | oldNode.appendChild(newChild) 125 | offset++ 126 | 127 | // Both nodes are the same, morph 128 | } else if (same(newChild, oldChild)) { 129 | morphed = walk(newChild, oldChild) 130 | if (morphed !== oldChild) { 131 | oldNode.replaceChild(morphed, oldChild) 132 | offset++ 133 | } 134 | 135 | // Both nodes do not share an ID or a placeholder, try reorder 136 | } else { 137 | oldMatch = null 138 | 139 | // Try and find a similar node somewhere in the tree 140 | for (var j = i; j < oldNode.childNodes.length; j++) { 141 | if (same(oldNode.childNodes[j], newChild)) { 142 | oldMatch = oldNode.childNodes[j] 143 | break 144 | } 145 | } 146 | 147 | // If there was a node with the same ID or placeholder in the old list 148 | if (oldMatch) { 149 | morphed = walk(newChild, oldMatch) 150 | if (morphed !== oldMatch) offset++ 151 | oldNode.insertBefore(morphed, oldChild) 152 | 153 | // It's safe to morph two nodes in-place if neither has an ID 154 | } else if (!newChild.id && !oldChild.id) { 155 | morphed = walk(newChild, oldChild) 156 | if (morphed !== oldChild) { 157 | oldNode.replaceChild(morphed, oldChild) 158 | offset++ 159 | } 160 | 161 | // Insert the node at the index if we couldn't morph or find a matching node 162 | } else { 163 | oldNode.insertBefore(newChild, oldChild) 164 | offset++ 165 | } 166 | } 167 | } 168 | } 169 | 170 | function same (a, b) { 171 | if (a.id) return a.id === b.id 172 | if (a.isSameNode) return a.isSameNode(b) 173 | if (a.tagName !== b.tagName) return false 174 | if (a.type === TEXT_NODE) return a.nodeValue === b.nodeValue 175 | return false 176 | } 177 | 178 | },{"./lib/morph":3,"assert":4}],2:[function(require,module,exports){ 179 | module.exports = [ 180 | // attribute events (can be set with attributes) 181 | 'onclick', 182 | 'ondblclick', 183 | 'onmousedown', 184 | 'onmouseup', 185 | 'onmouseover', 186 | 'onmousemove', 187 | 'onmouseout', 188 | 'onmouseenter', 189 | 'onmouseleave', 190 | 'ontouchcancel', 191 | 'ontouchend', 192 | 'ontouchmove', 193 | 'ontouchstart', 194 | 'ondragstart', 195 | 'ondrag', 196 | 'ondragenter', 197 | 'ondragleave', 198 | 'ondragover', 199 | 'ondrop', 200 | 'ondragend', 201 | 'onkeydown', 202 | 'onkeypress', 203 | 'onkeyup', 204 | 'onunload', 205 | 'onabort', 206 | 'onerror', 207 | 'onresize', 208 | 'onscroll', 209 | 'onselect', 210 | 'onchange', 211 | 'onsubmit', 212 | 'onreset', 213 | 'onfocus', 214 | 'onblur', 215 | 'oninput', 216 | // other common events 217 | 'oncontextmenu', 218 | 'onfocusin', 219 | 'onfocusout' 220 | ] 221 | 222 | },{}],3:[function(require,module,exports){ 223 | var events = require('./events') 224 | var eventsLength = events.length 225 | 226 | var ELEMENT_NODE = 1 227 | var TEXT_NODE = 3 228 | var COMMENT_NODE = 8 229 | 230 | module.exports = morph 231 | 232 | // diff elements and apply the resulting patch to the old node 233 | // (obj, obj) -> null 234 | function morph (newNode, oldNode) { 235 | var nodeType = newNode.nodeType 236 | var nodeName = newNode.nodeName 237 | 238 | if (nodeType === ELEMENT_NODE) { 239 | copyAttrs(newNode, oldNode) 240 | } 241 | 242 | if (nodeType === TEXT_NODE || nodeType === COMMENT_NODE) { 243 | if (oldNode.nodeValue !== newNode.nodeValue) { 244 | oldNode.nodeValue = newNode.nodeValue 245 | } 246 | } 247 | 248 | // Some DOM nodes are weird 249 | // https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js 250 | if (nodeName === 'INPUT') updateInput(newNode, oldNode) 251 | else if (nodeName === 'OPTION') updateOption(newNode, oldNode) 252 | else if (nodeName === 'TEXTAREA') updateTextarea(newNode, oldNode) 253 | 254 | copyEvents(newNode, oldNode) 255 | } 256 | 257 | function copyAttrs (newNode, oldNode) { 258 | var oldAttrs = oldNode.attributes 259 | var newAttrs = newNode.attributes 260 | var attrNamespaceURI = null 261 | var attrValue = null 262 | var fromValue = null 263 | var attrName = null 264 | var attr = null 265 | 266 | for (var i = newAttrs.length - 1; i >= 0; --i) { 267 | attr = newAttrs[i] 268 | attrName = attr.name 269 | attrNamespaceURI = attr.namespaceURI 270 | attrValue = attr.value 271 | if (attrNamespaceURI) { 272 | attrName = attr.localName || attrName 273 | fromValue = oldNode.getAttributeNS(attrNamespaceURI, attrName) 274 | if (fromValue !== attrValue) { 275 | oldNode.setAttributeNS(attrNamespaceURI, attrName, attrValue) 276 | } 277 | } else { 278 | if (!oldNode.hasAttribute(attrName)) { 279 | oldNode.setAttribute(attrName, attrValue) 280 | } else { 281 | fromValue = oldNode.getAttribute(attrName) 282 | if (fromValue !== attrValue) { 283 | // apparently values are always cast to strings, ah well 284 | if (attrValue === 'null' || attrValue === 'undefined') { 285 | oldNode.removeAttribute(attrName) 286 | } else { 287 | oldNode.setAttribute(attrName, attrValue) 288 | } 289 | } 290 | } 291 | } 292 | } 293 | 294 | // Remove any extra attributes found on the original DOM element that 295 | // weren't found on the target element. 296 | for (var j = oldAttrs.length - 1; j >= 0; --j) { 297 | attr = oldAttrs[j] 298 | if (attr.specified !== false) { 299 | attrName = attr.name 300 | attrNamespaceURI = attr.namespaceURI 301 | 302 | if (attrNamespaceURI) { 303 | attrName = attr.localName || attrName 304 | if (!newNode.hasAttributeNS(attrNamespaceURI, attrName)) { 305 | oldNode.removeAttributeNS(attrNamespaceURI, attrName) 306 | } 307 | } else { 308 | if (!newNode.hasAttributeNS(null, attrName)) { 309 | oldNode.removeAttribute(attrName) 310 | } 311 | } 312 | } 313 | } 314 | } 315 | 316 | function copyEvents (newNode, oldNode) { 317 | for (var i = 0; i < eventsLength; i++) { 318 | var ev = events[i] 319 | if (newNode[ev]) { // if new element has a whitelisted attribute 320 | oldNode[ev] = newNode[ev] // update existing element 321 | } else if (oldNode[ev]) { // if existing element has it and new one doesnt 322 | oldNode[ev] = undefined // remove it from existing element 323 | } 324 | } 325 | } 326 | 327 | function updateOption (newNode, oldNode) { 328 | updateAttribute(newNode, oldNode, 'selected') 329 | } 330 | 331 | // The "value" attribute is special for the element since it sets the 332 | // initial value. Changing the "value" attribute without changing the "value" 333 | // property will have no effect since it is only used to the set the initial 334 | // value. Similar for the "checked" attribute, and "disabled". 335 | function updateInput (newNode, oldNode) { 336 | var newValue = newNode.value 337 | var oldValue = oldNode.value 338 | 339 | updateAttribute(newNode, oldNode, 'checked') 340 | updateAttribute(newNode, oldNode, 'disabled') 341 | 342 | if (newValue !== oldValue) { 343 | oldNode.setAttribute('value', newValue) 344 | oldNode.value = newValue 345 | } 346 | 347 | if (newValue === 'null') { 348 | oldNode.value = '' 349 | oldNode.removeAttribute('value') 350 | } 351 | 352 | if (!newNode.hasAttributeNS(null, 'value')) { 353 | oldNode.removeAttribute('value') 354 | } else if (oldNode.type === 'range') { 355 | // this is so elements like slider move their UI thingy 356 | oldNode.value = newValue 357 | } 358 | } 359 | 360 | function updateTextarea (newNode, oldNode) { 361 | var newValue = newNode.value 362 | if (newValue !== oldNode.value) { 363 | oldNode.value = newValue 364 | } 365 | 366 | if (oldNode.firstChild && oldNode.firstChild.nodeValue !== newValue) { 367 | // Needed for IE. Apparently IE sets the placeholder as the 368 | // node value and vise versa. This ignores an empty update. 369 | if (newValue === '' && oldNode.firstChild.nodeValue === oldNode.placeholder) { 370 | return 371 | } 372 | 373 | oldNode.firstChild.nodeValue = newValue 374 | } 375 | } 376 | 377 | function updateAttribute (newNode, oldNode, name) { 378 | if (newNode[name] !== oldNode[name]) { 379 | oldNode[name] = newNode[name] 380 | if (newNode[name]) { 381 | oldNode.setAttribute(name, '') 382 | } else { 383 | oldNode.removeAttribute(name) 384 | } 385 | } 386 | } 387 | 388 | },{"./events":2}],4:[function(require,module,exports){ 389 | assert.notEqual = notEqual 390 | assert.notOk = notOk 391 | assert.equal = equal 392 | assert.ok = assert 393 | 394 | module.exports = assert 395 | 396 | function equal (a, b, m) { 397 | assert(a == b, m) // eslint-disable-line eqeqeq 398 | } 399 | 400 | function notEqual (a, b, m) { 401 | assert(a != b, m) // eslint-disable-line eqeqeq 402 | } 403 | 404 | function notOk (t, m) { 405 | assert(!t, m) 406 | } 407 | 408 | function assert (t, m) { 409 | if (!t) throw new Error(m || 'AssertionError') 410 | } 411 | 412 | },{}]},{},[1])(1) 413 | }); 414 | var {nanomorph} = global 415 | export {nanomorph as default} -------------------------------------------------------------------------------- /dev/webterm.css: -------------------------------------------------------------------------------- 1 | /*@import "../base/reset.less"; 2 | @import "../base/colors.less"; 3 | @import "../base/spacing.less"; 4 | @import "../base/typography.less"; 5 | 6 | @import "../com/spinner.less";*/ 7 | 8 | * { 9 | box-sizing: border-box; 10 | font-family: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif; 11 | } 12 | 13 | body { 14 | font-size: 14px; 15 | } 16 | 17 | main { 18 | padding: 1rem; 19 | } 20 | 21 | .output .entry { 22 | margin-bottom: 1rem; 23 | } 24 | 25 | .output .entry .entry-header { 26 | line-height: 1; 27 | font-size: 16px; 28 | } 29 | 30 | .output .entry .entry-content { 31 | white-space: pre; 32 | } 33 | 34 | .output .error { 35 | white-space: normal; 36 | color: red; 37 | } 38 | 39 | .output .error-stack { 40 | padding: 1rem; 41 | background: pink; 42 | font-weight: bold; 43 | font-size: 12px; 44 | } 45 | 46 | .prompt { 47 | display: flex; 48 | line-height: 1; 49 | font-size: 16px; 50 | } 51 | 52 | .prompt input { 53 | position: relative; 54 | top: -2px; 55 | flex: 1; 56 | line-height: 1; 57 | font-size: 16px; 58 | padding: 0 0 0 10px; 59 | outline: 0; 60 | border: 0; 61 | } -------------------------------------------------------------------------------- /dev/webterm.js: -------------------------------------------------------------------------------- 1 | import html from './vendor/nanohtml-v1.2.4.js' 2 | import morph from './vendor/nanomorph-v5.1.3.js' 3 | import minimist from './vendor/minimist-v1.2.0.js' 4 | import {importModule} from './vendor/dynamic-import-polyfill.js' 5 | import {joinPath} from './util.js' 6 | const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor 7 | 8 | // globals 9 | // = 10 | 11 | var cwd // current working directory. {url:, host:, pathname:, archive:} 12 | var env // current working environment 13 | 14 | var commandHist = { 15 | array: new Array(), 16 | insert: -1, 17 | cursor: -1, 18 | add (entry) { 19 | if (entry) { 20 | this.array.push(entry) 21 | } 22 | this.cursor = this.array.length 23 | }, 24 | prevUp () { 25 | if (this.cursor === -1) return '' 26 | this.cursor = Math.max(0, this.cursor - 1) 27 | return this.array[this.cursor] 28 | }, 29 | prevDown () { 30 | this.cursor = Math.min(this.array.length, this.cursor + 1) 31 | return this.array[this.cursor] || '' 32 | }, 33 | reset () { 34 | this.cursor = this.array.length 35 | } 36 | } 37 | 38 | // helper elem 39 | const gt = () => { 40 | var el = html`` 41 | el.innerHTML = '>' 42 | return el 43 | } 44 | 45 | // start 46 | // = 47 | 48 | document.addEventListener('keydown', setFocus, {capture: true}) 49 | document.addEventListener('keydown', onKeyDown, {capture: true}) 50 | window.addEventListener('focus', setFocus) 51 | readCWD() 52 | updatePrompt() 53 | importEnvironment() 54 | appendOutput(html`
Welcome to webterm. Type help if you get lost.
`, cwd.pathname) 55 | setFocus() 56 | 57 | // output 58 | // = 59 | 60 | function appendOutput (output, thenCWD, cmd) { 61 | if (typeof output === 'undefined') { 62 | output = 'Ok.' 63 | } else if (output.toHTML) { 64 | output = output.toHTML() 65 | } else if (typeof output !== 'string' && !(output instanceof Element)) { 66 | output = JSON.stringify(output).replace(/^"|"$/g, '') 67 | } 68 | thenCWD = thenCWD || cwd 69 | document.querySelector('.output').appendChild(html` 70 |
71 |
//${shortenHash(thenCWD.host)}${thenCWD.pathname}${gt()} ${cmd || ''}
72 |
${output}
73 |
74 | `) 75 | window.scrollTo(0, document.body.scrollHeight) 76 | } 77 | 78 | function appendError (msg, err, thenCWD, cmd) { 79 | appendOutput(html` 80 |
81 |
${msg}
82 |
${err.toString()}
83 |
84 | `, thenCWD, cmd) 85 | } 86 | 87 | function clearHistory () { 88 | document.querySelector('.output').innerHTML = '' 89 | } 90 | 91 | // prompt 92 | // 93 | 94 | function updatePrompt () { 95 | morph(document.querySelector('.prompt'), html` 96 |
97 | //${shortenHash(cwd.host)}${cwd.pathname}${gt()} 98 |
99 | `) 100 | } 101 | 102 | function shortenHash (str = '') { 103 | return str.replace(/[0-9a-f]{64}/ig, v => `${v.slice(0, 6)}..${v.slice(-2)}`) 104 | } 105 | 106 | function setFocus () { 107 | document.querySelector('.prompt input').focus() 108 | } 109 | 110 | function onKeyDown (e) { 111 | if (e.code === 'KeyL' && e.ctrlKey) { 112 | e.preventDefault() 113 | clearHistory() 114 | } else if (e.code === 'ArrowUp') { 115 | e.preventDefault() 116 | document.querySelector('.prompt input').value = commandHist.prevUp() 117 | } else if (e.code === 'ArrowDown') { 118 | e.preventDefault() 119 | document.querySelector('.prompt input').value = commandHist.prevDown() 120 | } else if (e.code === 'Escape') { 121 | e.preventDefault() 122 | document.querySelector('.prompt input').value = '' 123 | commandHist.reset() 124 | } 125 | } 126 | 127 | function onPromptKeyUp (e) { 128 | if (e.code === 'Enter') { 129 | evalPrompt() 130 | } 131 | } 132 | 133 | function evalPrompt () { 134 | var prompt = document.querySelector('.prompt input') 135 | if (!prompt.value.trim()) { 136 | return 137 | } 138 | commandHist.add(prompt.value) 139 | evalCommand(prompt.value) 140 | prompt.value = '' 141 | } 142 | 143 | function evalCommand (command) { 144 | evalCommandInternal(command, appendOutput, appendError, env, parseCommand, updatePrompt) 145 | } 146 | 147 | // use the func constructor to relax 'use strict' 148 | // that way we can use `with` 149 | var evalCommandInternal = new AsyncFunction('command', 'appendOutput', 'appendError', 'env', 'parseCommand', 'updatePrompt', ` 150 | try { 151 | var res 152 | var oldCWD = Object.assign({}, env.getCWD()) 153 | with (env) { 154 | res = await eval(parseCommand(command)) 155 | } 156 | appendOutput(res, oldCWD, command) 157 | } catch (err) { 158 | appendError('Command error', err, oldCWD, command) 159 | } 160 | updatePrompt() 161 | `) 162 | 163 | function parseCommand (str) { 164 | // parse the command 165 | var parts = str.split(' ') 166 | var cmd = parts[0] 167 | var argsParsed = minimist(parts.slice(1)) 168 | console.log(JSON.stringify(argsParsed)) 169 | 170 | // form the js call 171 | var args = argsParsed._ 172 | delete argsParsed._ 173 | args.unshift(argsParsed) // opts always go first 174 | 175 | console.log(`${cmd}(${args.map(JSON.stringify).join(', ')})`) 176 | return `${cmd}(${args.map(JSON.stringify).join(', ')})` 177 | } 178 | 179 | // environment 180 | // = 181 | 182 | async function importEnvironment () { 183 | document.head.append(html``) 184 | try { 185 | var module = await importModule('/dev/env-default.js') 186 | env = Object.assign({}, module) 187 | for (let k in builtins) { 188 | Object.defineProperty(env, k, {value: builtins[k], enumerable: false}) 189 | } 190 | window.env = env 191 | console.log('Environment', env) 192 | } catch (err) { 193 | console.error(err) 194 | return appendError('Failed to evaluate environment script', err, cwd) 195 | } 196 | } 197 | 198 | // current working location 199 | // = 200 | 201 | async function setCWD (location) { 202 | var locationParsed 203 | try { 204 | locationParsed = new URL(location) 205 | location = `${locationParsed.host}${locationParsed.pathname}` 206 | } catch (err) { 207 | location = `${cwd.host}${joinPath(cwd.pathname, location)}` 208 | } 209 | locationParsed = new URL('dat://' + location) 210 | 211 | // make sure the destination exists 212 | let archive = new DatArchive(locationParsed.host) 213 | let st = await archive.stat(locationParsed.pathname) 214 | if (!st.isDirectory()) { 215 | throw new Error('Not a directory') 216 | } 217 | 218 | window.history.pushState(null, {}, '#' + location) 219 | readCWD() 220 | } 221 | 222 | function readCWD () { 223 | cwd = parseURL(window.location.hash.slice(1) || window.location.toString()) 224 | 225 | console.log('CWD', cwd) 226 | document.title = `${cwd.host || cwd.url} | Terminal` 227 | } 228 | 229 | function parseURL (url) { 230 | if (!url.startsWith('dat://')) url = 'dat://' + url 231 | let urlp = new URL(url) 232 | let host = url.slice(0, url.indexOf('/')) 233 | let pathname = url.slice(url.indexOf('/')) 234 | let archive = new DatArchive(urlp.hostname) 235 | return {url, host: urlp.hostname, pathname: urlp.pathname, archive} 236 | } 237 | 238 | // builtins 239 | // = 240 | 241 | const builtins = { 242 | html, 243 | morph, 244 | evalCommand, 245 | getCWD () { 246 | return cwd 247 | }, 248 | setCWD 249 | } -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pfrazee/webterm/0bfd28218137199a44ffec101e28f7d3a1027fd8/favicon.ico -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Webterm 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 17 | 18 | 19 | 20 | --------------------------------------------------------------------------------