├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── Screenshot_20170524-134934.png ├── index.js ├── lib ├── api.js ├── client.js ├── render-file-list.js ├── web-code-stats.compiled.js ├── ws-routing.js └── ws.js ├── nohup.out ├── package-lock.json ├── package.json ├── rollup-isomorphic.config.js ├── rollup.config.js └── static ├── fonts ├── SamsungOne │ ├── SamsungOne-300.woff │ ├── SamsungOne-400.woff │ ├── SamsungOne-600.woff │ ├── SamsungOne-700.woff │ ├── SamsungOne-800.woff │ └── fonts.css └── flottflott │ ├── Flottflott.ttf │ ├── OFL-FAQ.txt │ ├── Open Font License.txt │ └── fonts.css ├── icon192.png ├── icon512.png ├── index.html ├── manifest.json ├── scripts ├── bundle.js ├── lib │ ├── buffer-file.js │ ├── db.js │ ├── errors.js │ ├── file-dialog.js │ ├── files.js │ ├── fs-proxy.js │ ├── monaco.js │ ├── newFile.js │ ├── render-file-list.js │ ├── side-bar.js │ ├── state.js │ ├── tab-controller.js │ ├── utils.js │ ├── web-code-stats.js │ └── ws.js ├── main.js └── sw-v1.js ├── styles ├── icons.css └── style.css └── sw-wrapper.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | web-code-3000.lock 3 | *.lock 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at ada@ada.is. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Samsung Internet Dev Rel 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 | # Deprecated 2 | 3 | I've recently started using [cdr/code-server](https://github.com/cdr/code-server) which is a full VS Code instance which now 4 | runs on Android. The same way this does. 5 | 6 | # Web Code 7 | 8 | A node based VSCode like editor. Made for Samsung DeX. 9 | 10 | ![Screenshot of Web Code on DeX](https://github.com/AdaRoseEdwards/web-code/blob/master/Screenshot_20170524-134934.png) 11 | 12 | # Installation 13 | 14 | In a Terminal (or Termux for Android) 15 | 16 | * Install node: 17 | ``` 18 | apt get update 19 | apt get install nodejs 20 | ``` 21 | 22 | * Install web-code 23 | ``` 24 | npm install -g web-code 25 | ``` 26 | 27 | * Run web-code 28 | ``` 29 | web-code ./my-file.js 30 | ``` 31 | 32 | # Using Web Code 33 | 34 | You open up web-code in your browser. 35 | 36 | go to: `http://127.0.0.1:3000` 37 | 38 | You can change the Web Code port from it's default of 3000 by running `PORT=8080 web-code` when starting a new instance of the web code daemon. 39 | 40 | Web Code will only run a single instance of the server but will reuse this instance for opening additional files and folders. 41 | 42 | You can open as many files/folders as you like by running `web-code foo.txt` and it will use the existing process. 43 | 44 | # Development 45 | 46 | 1. clone this repo 47 | 2. `npm install` 48 | 3. `npm run watch` 49 | 50 | # Your first PR 51 | 52 | Try adding an icon for a file format you like in: `static/styles/icons.css` only a few file formats have been mapped. To corresponding icons from atom file-icons. 53 | -------------------------------------------------------------------------------- /Screenshot_20170524-134934.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SamsungInternet/web-code/caf2401227497d00cf728d5ee3edcbd9972c0771/Screenshot_20170524-134934.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | /* eslint-env es6 */ 3 | 4 | const server = require('http').createServer(); 5 | const WebSocketServer = require('ws').Server; 6 | const wss = new WebSocketServer({ server: server }); 7 | const express = require('express'); 8 | const app = express(); 9 | const port = process.env.PORT || 3000; 10 | const api = require('./lib/api'); 11 | const wsMessaging = require('./lib/ws-routing'); 12 | const nodePath = require('path'); 13 | const Client = require('./lib/client'); 14 | const lockFile = require('lockfile'); 15 | const fs = require('fs'); 16 | const exec = require('child_process').exec; 17 | const pathIsInside = require('path-is-inside'); 18 | const Stats = require('./lib/web-code-stats.compiled'); 19 | const pathJoin = require('path').join; 20 | const pathDirname = require('path').dirname; 21 | 22 | function getModuleRoot(m) { 23 | return pathDirname(require.resolve(m + '/package.json')); 24 | } 25 | 26 | function puts(error, stdout, stderr) { console.log(stdout); console.log(stderr); } 27 | 28 | let lastWorkingDir = ''; 29 | const args = process.argv.slice(2).join(' ').trim(); 30 | const path = args && nodePath.resolve(args); 31 | lastWorkingDir = path || false; 32 | 33 | const lockfile = require('path').join(__dirname, 'web-code-' + port + '.lock'); 34 | lockFile.lock(lockfile, {}, function (err) { 35 | 36 | // There is a lock file running 37 | if (err) { 38 | 39 | // Check if the stored pid matches the current one 40 | const data = fs.readFileSync(lockfile, 'utf8').split('\n'); 41 | const storedDaemonPID = data[0]; 42 | let processIsRunning = true; 43 | 44 | try { 45 | process.kill(storedDaemonPID, 0); 46 | } catch (e) { 47 | processIsRunning = false; 48 | } 49 | 50 | // There is a daemon already running 51 | if (processIsRunning) { 52 | 53 | data[1] = lastWorkingDir; 54 | 55 | if (lastWorkingDir) { 56 | // Update the lockfile with new working dir, tell the daemon 57 | fs.writeFileSync(lockfile, data.join('\n')); 58 | 59 | // Daemon 60 | process.kill(storedDaemonPID, 'SIGPIPE'); 61 | } else { 62 | 63 | // Process already exists so try to message to it. 64 | console.log('Web-code daemon is already running, with pid:', storedDaemonPID); 65 | } 66 | 67 | return; 68 | } 69 | } 70 | 71 | fs.writeFileSync(lockfile, process.pid + '\n' + lastWorkingDir); 72 | 73 | app.use(express.static(__dirname + '/static', { 74 | maxAge: 3600 * 1000 * 24 75 | })); 76 | 77 | app.use(express.static(getModuleRoot('sw-toolbox'), { 78 | maxAge: 3600 * 1000 * 24 79 | })); 80 | 81 | app.use('/vs/', express.static(pathJoin(getModuleRoot('monaco-editor'), '/min/vs'), { 82 | maxAge: 3600 * 1000 * 24 83 | })); 84 | 85 | app.use('/icons/', express.static(pathJoin(getModuleRoot('file-icons'), '/fonts'), { 86 | maxAge: 3600 * 1000 * 24 87 | })); 88 | 89 | app.use('/axe/', express.static(getModuleRoot('axe-core'), { 90 | maxAge: 3600 * 1000 * 24 91 | })); 92 | 93 | app.use('/contextmenu/', express.static(getModuleRoot('contextmenu'), { 94 | maxAge: 3600 * 1000 * 24 95 | })); 96 | 97 | app.use('/fira/', express.static(__dirname + '/node_modules/FiraCode/distr', { 98 | maxAge: 3600 * 1000 * 24 99 | })); 100 | 101 | app.use('/api/', api); 102 | 103 | function heartbeat() { 104 | this.isAlive = true; 105 | } 106 | 107 | setInterval(function ping() { 108 | wss.clients.forEach(function each(ws) { 109 | if (ws.isAlive === false) return ws.terminate(); 110 | 111 | ws.isAlive = false; 112 | ws.ping('', false, true); 113 | }); 114 | }, 5000); 115 | 116 | wss.on('connection', function connection(ws) { 117 | ws.on('message', wsMessaging.wsRouting); 118 | 119 | ws.webCodeClient = new Client(); 120 | ws.webCodeClient.on('change', function fn(stats) { 121 | ws.send(JSON.stringify(['FS_CHANGE', null, stats.toDoc()]), e => e && console.log(e)); 122 | }); 123 | ws.webCodeClient.on('add', function fn(stats) { 124 | ws.send(JSON.stringify(['FS_ADD', null, stats.toDoc()]), e => e && console.log(e)); 125 | }); 126 | ws.webCodeClient.on('unlink', function fn(obj) { 127 | ws.send(JSON.stringify(['FS_UNLINK', null, obj]), e => e && console.log(e)); 128 | }); 129 | ws.webCodeClient.on('addDir', function fn(stats) { 130 | ws.send(JSON.stringify(['FS_ADD', null, stats.toDoc()]), e => e && console.log(e)); 131 | }); 132 | ws.webCodeClient.on('unlinkDir', function fn(obj) { 133 | ws.send(JSON.stringify(['FS_UNLINK', null, obj]), e => e && console.log(e)); 134 | }); 135 | 136 | ws.on('close', function close() { 137 | ws.webCodeClient.destroy(); 138 | ws.webCodeClient = null; 139 | }); 140 | 141 | ws.isAlive = true; 142 | ws.on('pong', heartbeat); 143 | 144 | ws.send(wsMessaging.wsSendFormat('HANDSHAKE', { 145 | path: lastWorkingDir || false 146 | })); 147 | 148 | }); 149 | 150 | server.on('request', app); 151 | server.listen(port, function () { 152 | /* eslint no-console: 0 */ 153 | console.log('Web Code Server running with PID:', process.pid); 154 | console.log('Open up: http://127.0.0.1:' + server.address().port); 155 | 156 | // if an address is set to open then do otherwise just run the server 157 | if (lastWorkingDir) exec('termux-open-url http://127.0.0.1:' + server.address().port, puts); 158 | }); 159 | 160 | function exit() { 161 | lockFile.unlockSync(lockfile); 162 | } 163 | 164 | process.on('SIGTERM', function() { 165 | process.exit(); 166 | }); 167 | 168 | process.on('SIGINT', function() { 169 | process.exit(); 170 | }); 171 | 172 | process.on('SIGPIPE', function() { 173 | const data = fs.readFileSync(lockfile, 'utf8').split('\n'); 174 | const path = nodePath.resolve(data[1]); 175 | const stats = fs.statSync(path); 176 | const statsObj = Stats.fromNodeStats(path, stats).toDoc(); 177 | 178 | // If it is a file in an already open window then open it there 179 | if (stats.isFile()) { 180 | for (const ws of wss.clients) { 181 | if (ws.editorState && ws.editorState.currentlyOpenedPath) { 182 | if(pathIsInside(path, ws.editorState.currentlyOpenedPath)) { 183 | ws.send(wsMessaging.wsSendFormat('OPEN_FILE', statsObj)); 184 | return; 185 | } 186 | } 187 | } 188 | } 189 | 190 | // Opening a new browser tab to that location. 191 | lastWorkingDir = path; 192 | exec('termux-open-url http://127.0.0.1:' + server.address().port, puts); 193 | }); 194 | 195 | process.on('exit', exit); 196 | }); 197 | -------------------------------------------------------------------------------- /lib/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-env es6 */ 2 | 3 | const express = require('express'); 4 | const app = express(); 5 | const bodyParser = require('body-parser'); 6 | 7 | app.use(bodyParser.json({})); 8 | 9 | function errorRes(str, res) { 10 | res.json(500, { 11 | error: str 12 | }); 13 | } 14 | 15 | 16 | app.use('/imageproxy', function (req,res) { 17 | const path = decodeURIComponent(req.query.url); 18 | res.sendFile(path); 19 | }); 20 | 21 | module.exports = app; -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | /* eslint-env es6 */ 2 | /* eslint no-console: 0 */ 3 | const chokidar = require('chokidar'); 4 | const p = require('path'); 5 | const EventEmitter = require('events'); 6 | const Stats = require('./web-code-stats.compiled.js'); 7 | 8 | class Client extends EventEmitter { 9 | constructor() { 10 | super(); 11 | } 12 | watchPath(path) { 13 | this.path = path; 14 | const watchPath = p.join(path); 15 | 16 | const self = this; 17 | 18 | if (this.watcher) this.watcher.close(); 19 | this.watcher = chokidar.watch(watchPath, { 20 | ignorePermissionErrors: true, 21 | alwaysStat: true, 22 | ignoreInitial: true, 23 | depth: 0 24 | }); 25 | function handleChanges(name, fileName, nodeStats) { 26 | if (!fileName) { 27 | return function (fileName, nodeStats) { 28 | handleChanges(name, fileName, nodeStats); 29 | } 30 | } else { 31 | if (nodeStats) { 32 | self.emit(name, Stats.fromNodeStats(fileName, nodeStats)); 33 | } else { 34 | self.emit(name, {path: fileName}); 35 | } 36 | } 37 | } 38 | 39 | //curried functions 40 | this.watcher.on('change', handleChanges('change')); 41 | this.watcher.on('add', handleChanges('add')); 42 | this.watcher.on('unlink', handleChanges('unlink')); 43 | this.watcher.on('addDir', handleChanges('addDir')); 44 | this.watcher.on('unlinkDir', handleChanges('unlinkDir')); 45 | } 46 | destroy() { 47 | if (this.watcher) this.watcher.close(); 48 | } 49 | } 50 | 51 | module.exports = Client; -------------------------------------------------------------------------------- /lib/render-file-list.js: -------------------------------------------------------------------------------- 1 | // Does nothing 2 | module.exports = {}; -------------------------------------------------------------------------------- /lib/web-code-stats.compiled.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /* eslint-disable */ 5 | var isServer = true; 6 | 7 | 8 | function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } 9 | 10 | var path = require('path'); 11 | var mime = _interopDefault(require('mime')); 12 | var renderFileList = _interopDefault(require('./render-file-list.js')); 13 | var __ws_js = require('./ws.js'); 14 | 15 | /* global Map, Set, Promise, fs, isServer */ 16 | /* eslint no-var: 0, no-console: 0 */ 17 | /* eslint-env es6 */ 18 | 19 | // Map to prevent duplicate data objects for each file 20 | var pathToDataMap = new Map(); 21 | 22 | var fsFromFn = ['isFile', 'isDirectory', 'isBlockDevice', 'isCharacterDevice', 'isSymbolicLink', 'isFIFO', 'isSocket']; 23 | var fsStatic = [ 24 | 'dev', 25 | 'mode', 26 | 'nlink', 27 | 'uid', 28 | 'gid', 29 | 'rdev', 30 | 'blksize', 31 | 'ino', 32 | 'size', 33 | 'blocks', 34 | 'atime', 35 | 'mtime', 36 | 'ctime', 37 | 'birthtime', 38 | 'path' 39 | ]; 40 | var keys = fsStatic.concat(fsFromFn); 41 | 42 | 43 | /** 44 | * Special type of singleton which returns the same object for each path. 45 | */ 46 | function Stats (data) { 47 | if (pathToDataMap.has(data.path)) { 48 | var existing = pathToDataMap.get(data.path); 49 | existing.update(data); 50 | return existing; 51 | } 52 | this.fileLists = new Set(); 53 | this.data = {}; 54 | this.update(data); 55 | pathToDataMap.set(data.path, this); 56 | } 57 | 58 | Stats.prototype.update = function update(data) { 59 | 60 | var self = this; 61 | 62 | this.data.name = path.basename(data.path); 63 | this.data.dirName = path.dirname(data.path); 64 | this.data.extension = path.extname(data.path).toLowerCase(); 65 | this.data.mime = data.isFile ? mime.lookup(data.path) : 'directory'; 66 | 67 | keys.forEach(function (key) { 68 | this.data[key] = data[key]; 69 | }.bind(this)); 70 | 71 | if (this.isDirectory() && !this.children) { 72 | this.children = []; 73 | this.childrenPopulated = false; 74 | } 75 | 76 | // Rerender file lists 77 | if (this.fileLists.size) { 78 | Array.from(this.fileLists).forEach(function (filelistEl) { 79 | filelistEl.innerHTML = ''; 80 | self.renderFileList(filelistEl, filelistEl.filelistOptions); 81 | }); 82 | } 83 | }; 84 | 85 | Stats.prototype.toDoc = function toDoc() { 86 | var out = { 87 | __webStatDoc: true 88 | }; 89 | keys.forEach(function (key) { 90 | out[key] = this.data[key]; 91 | }.bind(this)); 92 | return out; 93 | }; 94 | 95 | Stats.prototype.updateChildren = function () { 96 | if(!this.isDirectory()) throw Error('Not a directory'); 97 | 98 | var self = this; 99 | return fs.readdir(self.data.path) 100 | .then(function (arr) { 101 | return Promise.all(arr.map(function (child) { 102 | return Stats.fromPath(path.join(self.data.path, child)); 103 | })); 104 | }) 105 | .then(function (statsArray) { 106 | self.children.splice(0); 107 | self.children.push.apply(self.children, statsArray); 108 | 109 | // Let server know 110 | if (!isServer) __ws_js.remoteCmd('CLIENT', { 111 | cmd: 'watchPath', 112 | arguments: [self.data.path] 113 | }); 114 | 115 | self.update(self.data); 116 | 117 | return self; 118 | }); 119 | }; 120 | 121 | Stats.prototype.destroyFileList = function (el) { 122 | el.stats = undefined; 123 | this.fileLists.delete(el); 124 | el.innerHTML = ''; 125 | }; 126 | 127 | Stats.prototype.renderFileList = function (el, options) { 128 | 129 | el.filelistOptions = options; 130 | 131 | el.stats = this; 132 | this.fileLists.add(el); 133 | el.dataset.mime = this.data.mime; 134 | el.dataset.name = this.data.name; 135 | el.dataset.size = this.data.size; 136 | 137 | renderFileList(el, this.children, options); 138 | }; 139 | 140 | // add isFile isDirectory etc 141 | fsFromFn.forEach(function (key) { 142 | Stats.prototype[key] = new Function('return this.data["' + key + '"];'); 143 | }); 144 | 145 | Stats.fromPath = function (path$$1) { 146 | return fs.stat(path$$1); 147 | }; 148 | 149 | Stats.fromDoc = function (data) { 150 | return new Stats(data); 151 | }; 152 | 153 | Stats.fromNodeStats = function (path$$1, nodeStat) { 154 | 155 | var out = {}; 156 | 157 | fsFromFn.forEach(key => out[key] = nodeStat[key]()); 158 | keys.forEach(key => { 159 | if (typeof nodeStat[key] !== 'function' && typeof nodeStat[key] !== 'object') { 160 | out[key] = nodeStat[key]; 161 | } 162 | }); 163 | 164 | out.path = path.resolve(path$$1); 165 | 166 | return new Stats(out); 167 | }; 168 | 169 | module.exports = Stats; 170 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoid2ViLWNvZGUtc3RhdHMuY29tcGlsZWQuanMiLCJzb3VyY2VzIjpbIi4uL3N0YXRpYy9zY3JpcHRzL2xpYi93ZWItY29kZS1zdGF0cy5qcyJdLCJzb3VyY2VzQ29udGVudCI6WyIvKiBnbG9iYWwgTWFwLCBTZXQsIFByb21pc2UsIGZzLCBpc1NlcnZlciAqL1xuLyogZXNsaW50IG5vLXZhcjogMCwgbm8tY29uc29sZTogMCAqL1xuLyogZXNsaW50LWVudiBlczYgKi9cblxuaW1wb3J0IHsgcmVzb2x2ZSBhcyBwYXRoUmVzb2x2ZSwgYmFzZW5hbWUsIGRpcm5hbWUsIGV4dG5hbWUsIGpvaW4gfSBmcm9tICdwYXRoJztcbmltcG9ydCBtaW1lIGZyb20gJ21pbWUnO1xuaW1wb3J0IHJlbmRlckZpbGVMaXN0IGZyb20gJy4vcmVuZGVyLWZpbGUtbGlzdC5qcyc7XG5pbXBvcnQgeyByZW1vdGVDbWQgfSBmcm9tICcuL3dzLmpzJztcblxuLy8gTWFwIHRvIHByZXZlbnQgZHVwbGljYXRlIGRhdGEgb2JqZWN0cyBmb3IgZWFjaCBmaWxlXG52YXIgcGF0aFRvRGF0YU1hcCA9IG5ldyBNYXAoKTtcblxudmFyIGZzRnJvbUZuID0gWydpc0ZpbGUnLCAnaXNEaXJlY3RvcnknLCAnaXNCbG9ja0RldmljZScsICdpc0NoYXJhY3RlckRldmljZScsICdpc1N5bWJvbGljTGluaycsICdpc0ZJRk8nLCAnaXNTb2NrZXQnXTtcbnZhciBmc1N0YXRpYyA9IFtcblx0J2RldicsXG5cdCdtb2RlJyxcblx0J25saW5rJyxcblx0J3VpZCcsXG5cdCdnaWQnLFxuXHQncmRldicsXG5cdCdibGtzaXplJyxcblx0J2lubycsXG5cdCdzaXplJyxcblx0J2Jsb2NrcycsXG5cdCdhdGltZScsXG5cdCdtdGltZScsXG5cdCdjdGltZScsXG5cdCdiaXJ0aHRpbWUnLFxuXHQncGF0aCdcbl07XG52YXIga2V5cyA9IGZzU3RhdGljLmNvbmNhdChmc0Zyb21Gbik7XG5cblxuLyoqXG4gKiBTcGVjaWFsIHR5cGUgb2Ygc2luZ2xldG9uIHdoaWNoIHJldHVybnMgdGhlIHNhbWUgb2JqZWN0IGZvciBlYWNoIHBhdGguXG4gKi9cbmV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIFN0YXRzIChkYXRhKSB7XG5cdGlmIChwYXRoVG9EYXRhTWFwLmhhcyhkYXRhLnBhdGgpKSB7XG5cdFx0dmFyIGV4aXN0aW5nID0gcGF0aFRvRGF0YU1hcC5nZXQoZGF0YS5wYXRoKTtcblx0XHRleGlzdGluZy51cGRhdGUoZGF0YSk7XG5cdFx0cmV0dXJuIGV4aXN0aW5nO1xuXHR9XG5cdHRoaXMuZmlsZUxpc3RzID0gbmV3IFNldCgpO1xuXHR0aGlzLmRhdGEgPSB7fTtcblx0dGhpcy51cGRhdGUoZGF0YSk7XG5cdHBhdGhUb0RhdGFNYXAuc2V0KGRhdGEucGF0aCwgdGhpcyk7XG59XG5cblN0YXRzLnByb3RvdHlwZS51cGRhdGUgPSBmdW5jdGlvbiB1cGRhdGUoZGF0YSkge1xuXG5cdHZhciBzZWxmID0gdGhpcztcblxuXHR0aGlzLmRhdGEubmFtZSA9IGJhc2VuYW1lKGRhdGEucGF0aCk7XG5cdHRoaXMuZGF0YS5kaXJOYW1lID0gZGlybmFtZShkYXRhLnBhdGgpO1xuXHR0aGlzLmRhdGEuZXh0ZW5zaW9uID0gZXh0bmFtZShkYXRhLnBhdGgpLnRvTG93ZXJDYXNlKCk7XG5cdHRoaXMuZGF0YS5taW1lID0gZGF0YS5pc0ZpbGUgPyBtaW1lLmxvb2t1cChkYXRhLnBhdGgpIDogJ2RpcmVjdG9yeSc7XG5cblx0a2V5cy5mb3JFYWNoKGZ1bmN0aW9uIChrZXkpIHtcblx0XHR0aGlzLmRhdGFba2V5XSA9IGRhdGFba2V5XTtcblx0fS5iaW5kKHRoaXMpKTtcblxuXHRpZiAodGhpcy5pc0RpcmVjdG9yeSgpICYmICF0aGlzLmNoaWxkcmVuKSB7XG5cdFx0dGhpcy5jaGlsZHJlbiA9IFtdO1xuXHRcdHRoaXMuY2hpbGRyZW5Qb3B1bGF0ZWQgPSBmYWxzZTtcblx0fVxuXG4gICAgLy8gUmVyZW5kZXIgZmlsZSBsaXN0c1xuXHRpZiAodGhpcy5maWxlTGlzdHMuc2l6ZSkge1xuXHRcdEFycmF5LmZyb20odGhpcy5maWxlTGlzdHMpLmZvckVhY2goZnVuY3Rpb24gKGZpbGVsaXN0RWwpIHtcblx0XHRcdGZpbGVsaXN0RWwuaW5uZXJIVE1MID0gJyc7XG5cdFx0XHRzZWxmLnJlbmRlckZpbGVMaXN0KGZpbGVsaXN0RWwsIGZpbGVsaXN0RWwuZmlsZWxpc3RPcHRpb25zKTtcblx0XHR9KTtcblx0fVxufVxuXG5TdGF0cy5wcm90b3R5cGUudG9Eb2MgPSBmdW5jdGlvbiB0b0RvYygpIHtcblx0dmFyIG91dCA9IHtcblx0XHRfX3dlYlN0YXREb2M6IHRydWVcblx0fTtcblx0a2V5cy5mb3JFYWNoKGZ1bmN0aW9uIChrZXkpIHtcblx0XHRvdXRba2V5XSA9IHRoaXMuZGF0YVtrZXldO1xuXHR9LmJpbmQodGhpcykpO1xuXHRyZXR1cm4gb3V0O1xufVxuXG5TdGF0cy5wcm90b3R5cGUudXBkYXRlQ2hpbGRyZW4gPSBmdW5jdGlvbiAoKSB7XG5cdGlmKCF0aGlzLmlzRGlyZWN0b3J5KCkpIHRocm93IEVycm9yKCdOb3QgYSBkaXJlY3RvcnknKTtcblxuXHR2YXIgc2VsZiA9IHRoaXM7XG5cdHJldHVybiBmcy5yZWFkZGlyKHNlbGYuZGF0YS5wYXRoKVxuXHQudGhlbihmdW5jdGlvbiAoYXJyKSB7XG5cdFx0cmV0dXJuIFByb21pc2UuYWxsKGFyci5tYXAoZnVuY3Rpb24gKGNoaWxkKSB7XG5cdFx0XHRyZXR1cm4gU3RhdHMuZnJvbVBhdGgoam9pbihzZWxmLmRhdGEucGF0aCwgY2hpbGQpKTtcblx0XHR9KSk7XG5cdH0pXG5cdC50aGVuKGZ1bmN0aW9uIChzdGF0c0FycmF5KSB7XG5cdFx0c2VsZi5jaGlsZHJlbi5zcGxpY2UoMCk7XG5cdFx0c2VsZi5jaGlsZHJlbi5wdXNoLmFwcGx5KHNlbGYuY2hpbGRyZW4sIHN0YXRzQXJyYXkpO1xuXG5cdFx0Ly8gTGV0IHNlcnZlciBrbm93XHRcblx0XHRpZiAoIWlzU2VydmVyKSByZW1vdGVDbWQoJ0NMSUVOVCcsIHtcblx0XHRcdGNtZDogJ3dhdGNoUGF0aCcsXG5cdFx0XHRhcmd1bWVudHM6IFtzZWxmLmRhdGEucGF0aF1cblx0XHR9KTtcblxuXHRcdHNlbGYudXBkYXRlKHNlbGYuZGF0YSk7XG5cblx0XHRyZXR1cm4gc2VsZjsgIFxuXHR9KTtcbn1cblxuU3RhdHMucHJvdG90eXBlLmRlc3Ryb3lGaWxlTGlzdCA9IGZ1bmN0aW9uIChlbCkge1xuXHRlbC5zdGF0cyA9IHVuZGVmaW5lZDtcblx0dGhpcy5maWxlTGlzdHMuZGVsZXRlKGVsKTtcblx0ZWwuaW5uZXJIVE1MID0gJyc7XG59XG5cblN0YXRzLnByb3RvdHlwZS5yZW5kZXJGaWxlTGlzdCA9IGZ1bmN0aW9uIChlbCwgb3B0aW9ucykge1xuXG5cdGVsLmZpbGVsaXN0T3B0aW9ucyA9IG9wdGlvbnM7XG5cblx0ZWwuc3RhdHMgPSB0aGlzO1xuXHR0aGlzLmZpbGVMaXN0cy5hZGQoZWwpO1xuXHRlbC5kYXRhc2V0Lm1pbWUgPSB0aGlzLmRhdGEubWltZTtcblx0ZWwuZGF0YXNldC5uYW1lID0gdGhpcy5kYXRhLm5hbWU7XG5cdGVsLmRhdGFzZXQuc2l6ZSA9IHRoaXMuZGF0YS5zaXplO1xuXG5cdHJlbmRlckZpbGVMaXN0KGVsLCB0aGlzLmNoaWxkcmVuLCBvcHRpb25zKTtcbn1cblxuLy8gYWRkIGlzRmlsZSBpc0RpcmVjdG9yeSBldGNcbmZzRnJvbUZuLmZvckVhY2goZnVuY3Rpb24gKGtleSkge1xuXHRTdGF0cy5wcm90b3R5cGVba2V5XSA9IG5ldyBGdW5jdGlvbigncmV0dXJuIHRoaXMuZGF0YVtcIicgKyBrZXkgKyAnXCJdOycpO1xufSk7XG5cblN0YXRzLmZyb21QYXRoID0gZnVuY3Rpb24gKHBhdGgpIHtcblx0cmV0dXJuIGZzLnN0YXQocGF0aCk7XG59XG5cblN0YXRzLmZyb21Eb2MgPSBmdW5jdGlvbiAoZGF0YSkge1xuXHRyZXR1cm4gbmV3IFN0YXRzKGRhdGEpO1xufVxuXG5TdGF0cy5mcm9tTm9kZVN0YXRzID0gZnVuY3Rpb24gKHBhdGgsIG5vZGVTdGF0KSB7XG5cblx0dmFyIG91dCA9IHt9O1xuXG5cdGZzRnJvbUZuLmZvckVhY2goa2V5ID0+IG91dFtrZXldID0gbm9kZVN0YXRba2V5XSgpKTtcblx0a2V5cy5mb3JFYWNoKGtleSA9PiB7XG5cdFx0aWYgKHR5cGVvZiBub2RlU3RhdFtrZXldICE9PSAnZnVuY3Rpb24nICYmIHR5cGVvZiBub2RlU3RhdFtrZXldICE9PSAnb2JqZWN0Jykge1xuXHRcdFx0b3V0W2tleV0gPSBub2RlU3RhdFtrZXldO1xuXHRcdH1cblx0fSk7XG5cblx0b3V0LnBhdGggPSBwYXRoUmVzb2x2ZShwYXRoKTtcblxuXHRyZXR1cm4gbmV3IFN0YXRzKG91dCk7XG59XG4iXSwibmFtZXMiOlsiYmFzZW5hbWUiLCJkaXJuYW1lIiwiZXh0bmFtZSIsImpvaW4iLCJyZW1vdGVDbWQiLCJwYXRoIiwicGF0aFJlc29sdmUiXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7O0FBQUE7Ozs7QUFJQSxBQUtBO0FBQ0EsSUFBSSxhQUFhLEdBQUcsSUFBSSxHQUFHLEVBQUUsQ0FBQzs7QUFFOUIsSUFBSSxRQUFRLEdBQUcsQ0FBQyxRQUFRLEVBQUUsYUFBYSxFQUFFLGVBQWUsRUFBRSxtQkFBbUIsRUFBRSxnQkFBZ0IsRUFBRSxRQUFRLEVBQUUsVUFBVSxDQUFDLENBQUM7QUFDdkgsSUFBSSxRQUFRLEdBQUc7Q0FDZCxLQUFLO0NBQ0wsTUFBTTtDQUNOLE9BQU87Q0FDUCxLQUFLO0NBQ0wsS0FBSztDQUNMLE1BQU07Q0FDTixTQUFTO0NBQ1QsS0FBSztDQUNMLE1BQU07Q0FDTixRQUFRO0NBQ1IsT0FBTztDQUNQLE9BQU87Q0FDUCxPQUFPO0NBQ1AsV0FBVztDQUNYLE1BQU07Q0FDTixDQUFDO0FBQ0YsSUFBSSxJQUFJLEdBQUcsUUFBUSxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsQ0FBQzs7Ozs7O0FBTXJDLEFBQWUsU0FBUyxLQUFLLEVBQUUsSUFBSSxFQUFFO0NBQ3BDLElBQUksYUFBYSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLEVBQUU7RUFDakMsSUFBSSxRQUFRLEdBQUcsYUFBYSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7RUFDNUMsUUFBUSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQztFQUN0QixPQUFPLFFBQVEsQ0FBQztFQUNoQjtDQUNELElBQUksQ0FBQyxTQUFTLEdBQUcsSUFBSSxHQUFHLEVBQUUsQ0FBQztDQUMzQixJQUFJLENBQUMsSUFBSSxHQUFHLEVBQUUsQ0FBQztDQUNmLElBQUksQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUM7Q0FDbEIsYUFBYSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsSUFBSSxFQUFFLElBQUksQ0FBQyxDQUFDO0NBQ25DOztBQUVELEtBQUssQ0FBQyxTQUFTLENBQUMsTUFBTSxHQUFHLFNBQVMsTUFBTSxDQUFDLElBQUksRUFBRTs7Q0FFOUMsSUFBSSxJQUFJLEdBQUcsSUFBSSxDQUFDOztDQUVoQixJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksR0FBR0EsYUFBUSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztDQUNyQyxJQUFJLENBQUMsSUFBSSxDQUFDLE9BQU8sR0FBR0MsWUFBTyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztDQUN2QyxJQUFJLENBQUMsSUFBSSxDQUFDLFNBQVMsR0FBR0MsWUFBTyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQyxXQUFXLEVBQUUsQ0FBQztDQUN2RCxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksR0FBRyxJQUFJLENBQUMsTUFBTSxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxHQUFHLFdBQVcsQ0FBQzs7Q0FFcEUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxVQUFVLEdBQUcsRUFBRTtFQUMzQixJQUFJLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQztFQUMzQixDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDOztDQUVkLElBQUksSUFBSSxDQUFDLFdBQVcsRUFBRSxJQUFJLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRTtFQUN6QyxJQUFJLENBQUMsUUFBUSxHQUFHLEVBQUUsQ0FBQztFQUNuQixJQUFJLENBQUMsaUJBQWlCLEdBQUcsS0FBSyxDQUFDO0VBQy9COzs7Q0FHRCxJQUFJLElBQUksQ0FBQyxTQUFTLENBQUMsSUFBSSxFQUFFO0VBQ3hCLEtBQUssQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxVQUFVLFVBQVUsRUFBRTtHQUN4RCxVQUFVLENBQUMsU0FBUyxHQUFHLEVBQUUsQ0FBQztHQUMxQixJQUFJLENBQUMsY0FBYyxDQUFDLFVBQVUsRUFBRSxVQUFVLENBQUMsZUFBZSxDQUFDLENBQUM7R0FDNUQsQ0FBQyxDQUFDO0VBQ0g7RUFDRDs7QUFFRCxLQUFLLENBQUMsU0FBUyxDQUFDLEtBQUssR0FBRyxTQUFTLEtBQUssR0FBRztDQUN4QyxJQUFJLEdBQUcsR0FBRztFQUNULFlBQVksRUFBRSxJQUFJO0VBQ2xCLENBQUM7Q0FDRixJQUFJLENBQUMsT0FBTyxDQUFDLFVBQVUsR0FBRyxFQUFFO0VBQzNCLEdBQUcsQ0FBQyxHQUFHLENBQUMsR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDO0VBQzFCLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUM7Q0FDZCxPQUFPLEdBQUcsQ0FBQztFQUNYOztBQUVELEtBQUssQ0FBQyxTQUFTLENBQUMsY0FBYyxHQUFHLFlBQVk7Q0FDNUMsR0FBRyxDQUFDLElBQUksQ0FBQyxXQUFXLEVBQUUsRUFBRSxNQUFNLEtBQUssQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDOztDQUV2RCxJQUFJLElBQUksR0FBRyxJQUFJLENBQUM7Q0FDaEIsT0FBTyxFQUFFLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDO0VBQ2hDLElBQUksQ0FBQyxVQUFVLEdBQUcsRUFBRTtFQUNwQixPQUFPLE9BQU8sQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxVQUFVLEtBQUssRUFBRTtHQUMzQyxPQUFPLEtBQUssQ0FBQyxRQUFRLENBQUNDLFNBQUksQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksRUFBRSxLQUFLLENBQUMsQ0FBQyxDQUFDO0dBQ25ELENBQUMsQ0FBQyxDQUFDO0VBQ0osQ0FBQztFQUNELElBQUksQ0FBQyxVQUFVLFVBQVUsRUFBRTtFQUMzQixJQUFJLENBQUMsUUFBUSxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQztFQUN4QixJQUFJLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxVQUFVLENBQUMsQ0FBQzs7O0VBR3BELElBQUksQ0FBQyxRQUFRLEVBQUVDLGlCQUFTLENBQUMsUUFBUSxFQUFFO0dBQ2xDLEdBQUcsRUFBRSxXQUFXO0dBQ2hCLFNBQVMsRUFBRSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDO0dBQzNCLENBQUMsQ0FBQzs7RUFFSCxJQUFJLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQzs7RUFFdkIsT0FBTyxJQUFJLENBQUM7RUFDWixDQUFDLENBQUM7RUFDSDs7QUFFRCxLQUFLLENBQUMsU0FBUyxDQUFDLGVBQWUsR0FBRyxVQUFVLEVBQUUsRUFBRTtDQUMvQyxFQUFFLENBQUMsS0FBSyxHQUFHLFNBQVMsQ0FBQztDQUNyQixJQUFJLENBQUMsU0FBUyxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUMsQ0FBQztDQUMxQixFQUFFLENBQUMsU0FBUyxHQUFHLEVBQUUsQ0FBQztFQUNsQjs7QUFFRCxLQUFLLENBQUMsU0FBUyxDQUFDLGNBQWMsR0FBRyxVQUFVLEVBQUUsRUFBRSxPQUFPLEVBQUU7O0NBRXZELEVBQUUsQ0FBQyxlQUFlLEdBQUcsT0FBTyxDQUFDOztDQUU3QixFQUFFLENBQUMsS0FBSyxHQUFHLElBQUksQ0FBQztDQUNoQixJQUFJLENBQUMsU0FBUyxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUMsQ0FBQztDQUN2QixFQUFFLENBQUMsT0FBTyxDQUFDLElBQUksR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQztDQUNqQyxFQUFFLENBQUMsT0FBTyxDQUFDLElBQUksR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQztDQUNqQyxFQUFFLENBQUMsT0FBTyxDQUFDLElBQUksR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQzs7Q0FFakMsY0FBYyxDQUFDLEVBQUUsRUFBRSxJQUFJLENBQUMsUUFBUSxFQUFFLE9BQU8sQ0FBQyxDQUFDO0VBQzNDOzs7QUFHRCxRQUFRLENBQUMsT0FBTyxDQUFDLFVBQVUsR0FBRyxFQUFFO0NBQy9CLEtBQUssQ0FBQyxTQUFTLENBQUMsR0FBRyxDQUFDLEdBQUcsSUFBSSxRQUFRLENBQUMsb0JBQW9CLEdBQUcsR0FBRyxHQUFHLEtBQUssQ0FBQyxDQUFDO0NBQ3hFLENBQUMsQ0FBQzs7QUFFSCxLQUFLLENBQUMsUUFBUSxHQUFHLFVBQVVDLE9BQUksRUFBRTtDQUNoQyxPQUFPLEVBQUUsQ0FBQyxJQUFJLENBQUNBLE9BQUksQ0FBQyxDQUFDO0VBQ3JCOztBQUVELEtBQUssQ0FBQyxPQUFPLEdBQUcsVUFBVSxJQUFJLEVBQUU7Q0FDL0IsT0FBTyxJQUFJLEtBQUssQ0FBQyxJQUFJLENBQUMsQ0FBQztFQUN2Qjs7QUFFRCxLQUFLLENBQUMsYUFBYSxHQUFHLFVBQVVBLE9BQUksRUFBRSxRQUFRLEVBQUU7O0NBRS9DLElBQUksR0FBRyxHQUFHLEVBQUUsQ0FBQzs7Q0FFYixRQUFRLENBQUMsT0FBTyxDQUFDLEdBQUcsSUFBSSxHQUFHLENBQUMsR0FBRyxDQUFDLEdBQUcsUUFBUSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUMsQ0FBQztDQUNwRCxJQUFJLENBQUMsT0FBTyxDQUFDLEdBQUcsSUFBSTtFQUNuQixJQUFJLE9BQU8sUUFBUSxDQUFDLEdBQUcsQ0FBQyxLQUFLLFVBQVUsSUFBSSxPQUFPLFFBQVEsQ0FBQyxHQUFHLENBQUMsS0FBSyxRQUFRLEVBQUU7R0FDN0UsR0FBRyxDQUFDLEdBQUcsQ0FBQyxHQUFHLFFBQVEsQ0FBQyxHQUFHLENBQUMsQ0FBQztHQUN6QjtFQUNELENBQUMsQ0FBQzs7Q0FFSCxHQUFHLENBQUMsSUFBSSxHQUFHQyxZQUFXLENBQUNELE9BQUksQ0FBQyxDQUFDOztDQUU3QixPQUFPLElBQUksS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDO0NBQ3RCOzs7OyJ9 171 | -------------------------------------------------------------------------------- /lib/ws-routing.js: -------------------------------------------------------------------------------- 1 | /* eslint-env es6 */ 2 | 3 | const fs = require('fs'); 4 | const Stats = require('./web-code-stats.compiled'); 5 | 6 | function sanitise(data) { 7 | if (typeof data === 'object' && data.constructor !== Array) { 8 | if (data.constructor === Stats) { 9 | return data.toDoc(); 10 | } 11 | throw Error('Not parsable yet'); 12 | } 13 | return data; 14 | } 15 | 16 | // The server bit of FSProxy, may need to simplify output 17 | function fsProxy(data) { 18 | return new Promise(function (resolve, reject) { 19 | data.arguments.push(function (err, out) { 20 | if (err) { 21 | return reject(err); 22 | } 23 | 24 | if (out === undefined) { 25 | return resolve(); 26 | } 27 | 28 | // convert fs.Stats to local stat which can be serialised 29 | if (out.constructor === fs.Stats) { 30 | 31 | // The first argument is the path needed when constructing from node 32 | return resolve(sanitise(Stats.fromNodeStats(data.arguments[0], out))); 33 | } 34 | resolve(sanitise(out)); 35 | }); 36 | fs[data.cmd].apply(fs, data.arguments) 37 | }); 38 | } 39 | 40 | function fetchEnvVar(name) { 41 | return Promise.resolve(process.env[name]); 42 | } 43 | 44 | function client(client, data) { 45 | data.arguments = data.arguments || []; 46 | return new Promise(function (resolve) { 47 | resolve(client[data.cmd].apply(client, data.arguments)); 48 | }); 49 | } 50 | 51 | module.exports = { 52 | wsRouting: function wsRouting(message) { 53 | if (message === '__ping__') { 54 | this.send('__pong__'); 55 | return; 56 | } 57 | const [cmd, id, data] = JSON.parse(message); 58 | if(typeof data === "object" && 59 | typeof data.arguments === "object" && 60 | data.arguments[0] === false) 61 | return; 62 | const ws = this; 63 | let promise; 64 | switch (cmd) { 65 | case 'SYNC_STATE': 66 | ws.editorState = data; 67 | promise = Promise.resolve(); 68 | break; 69 | case 'FS_PROXY': promise = fsProxy(data); break; 70 | case 'GET_ENV': promise = fetchEnvVar(data); break; 71 | case 'CLIENT': promise = client(this.webCodeClient, data); break; 72 | default: promise = Promise.reject(Error('No command ' + cmd)); 73 | } 74 | promise.then(function (data) { 75 | ws.send(JSON.stringify([ 76 | cmd, 77 | id, 78 | { result: data } 79 | ])); 80 | }) 81 | .catch(function (e) { 82 | ws.send(JSON.stringify([ 83 | cmd, 84 | id, 85 | {error: e.message} 86 | ])); 87 | }); 88 | }, 89 | wsSendFormat: function (cmd, data) { 90 | const id = Date.now() + '_' + Math.random(); 91 | return JSON.stringify([ 92 | cmd, 93 | id, 94 | data 95 | ]); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/ws.js: -------------------------------------------------------------------------------- 1 | // Does nothing 2 | module.exports = {}; -------------------------------------------------------------------------------- /nohup.out: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SamsungInternet/web-code/caf2401227497d00cf728d5ee3edcbd9972c0771/nohup.out -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-code", 3 | "version": "2.6.2", 4 | "description": "Text editor for DeX and the Web based around Monaco", 5 | "main": "index.js", 6 | "preferGlobal": true, 7 | "bin": { 8 | "web-code": "./index.js" 9 | }, 10 | "scripts": { 11 | "install": "termux-fix-shebang $(which rollup) || echo 'Not in termux'; termux-fix-shebang $(which napa) || echo 'Not in termux'; napa tonsky/FiraCode", 12 | "build": "rollup -c;", 13 | "build-server": "rollup -c rollup-isomorphic.config.js", 14 | "test": "echo \"Error: no test specified\" && exit 1", 15 | "watch": "nodemon --ignore static/scripts/bundle.js --ignore lib/web-code-stats.compiled.js --exec \"rm web-code-3000.lock; npm run build; npm run build-server; DEBUG=1 npm run start\"", 16 | "postinstall": "termux-fix-shebang ./index.js || echo 'Not in termux'", 17 | "start": "node ./" 18 | }, 19 | "author": "Ada Rose Edwards", 20 | "license": "MIT", 21 | "dependencies": { 22 | "axe-core": "^2.3.1", 23 | "body-parser": "^1.19.0", 24 | "chokidar": "^1.7.0", 25 | "contextmenu": "github:aantthony/contextmenu", 26 | "express": "^4.17.1", 27 | "file-icons": "git+https://github.com/file-icons/atom.git", 28 | "lockfile": "^1.0.3", 29 | "lodash.debounce": "^4.0.8", 30 | "mime": "^1.6.0", 31 | "monaco": "^1.201704190613.0", 32 | "monaco-editor": "^0.18.1", 33 | "napa": "^3.0.0", 34 | "npm-check-updates": "^2.12.1", 35 | "path-is-inside": "^1.0.2", 36 | "pouchdb-browser": "^6.3.4", 37 | "rollup-plugin-commonjs": "^8.1.0", 38 | "sw-toolbox": "^3.6.0", 39 | "ws": "^3.3.3" 40 | }, 41 | "devDependencies": { 42 | "nodemon": "^1.19.4", 43 | "rollup": "^0.47.4", 44 | "rollup-plugin-commonjs": "^8.1.0", 45 | "rollup-plugin-json": "^2.3.0", 46 | "rollup-plugin-node-builtins": "^2.1.2", 47 | "rollup-plugin-node-resolve": "^3.0.0", 48 | "rollup-plugin-sourcemaps": "^0.4.2" 49 | }, 50 | "eslintConfig": { 51 | "extends": [ 52 | "plugin:import/errors" 53 | ], 54 | "parserOptions": { 55 | "sourceType": "module" 56 | }, 57 | "env": { 58 | "browser": true, 59 | "mocha": true, 60 | "node": true 61 | }, 62 | "rules": { 63 | "no-unused-vars": 2, 64 | "no-undef": 2, 65 | "eqeqeq": 2, 66 | "no-underscore-dangle": 0, 67 | "guard-for-in": 2, 68 | "no-extend-native": 2, 69 | "wrap-iife": 2, 70 | "new-cap": 2, 71 | "no-caller": 2, 72 | "quotes": [ 73 | 1, 74 | "single" 75 | ], 76 | "indent": [ 77 | "error", 78 | "tab" 79 | ], 80 | "no-loop-func": 2, 81 | "no-irregular-whitespace": 2, 82 | "no-multi-spaces": 2, 83 | "one-var": [ 84 | 2, 85 | "never" 86 | ], 87 | "no-var": 1, 88 | "strict": [ 89 | 1, 90 | "global" 91 | ], 92 | "no-console": 1 93 | }, 94 | "root": true 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /rollup-isomorphic.config.js: -------------------------------------------------------------------------------- 1 | // rollup.config.js 2 | import 'rollup'; /* eslint no-unused-vars: 0*/ 3 | import * as path from 'path'; 4 | 5 | export default { 6 | entry: 'static/scripts/lib/web-code-stats.js', 7 | dest: 'lib/web-code-stats.compiled.js', 8 | format: 'cjs', 9 | sourceMap: 'inline', 10 | external: [ 11 | path.resolve( './static/scripts/lib/render-file-list.js' ), 12 | path.resolve( './static/scripts/lib/ws.js' ), 13 | 'path', 'mime' 14 | ], 15 | intro: ` 16 | /* eslint-disable */ 17 | var isServer = true; 18 | `, 19 | plugins: [] 20 | }; 21 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // rollup.config.js 2 | import 'rollup'; /* eslint no-unused-vars: 0*/ 3 | import resolve from 'rollup-plugin-node-resolve'; 4 | import commonjs from 'rollup-plugin-commonjs'; 5 | import builtins from 'rollup-plugin-node-builtins'; 6 | import json from 'rollup-plugin-json'; 7 | 8 | export default { 9 | entry: 'static/scripts/main.js', 10 | dest: 'static/scripts/bundle.js', 11 | format: 'iife', 12 | sourceMap: 'inline', 13 | intro: ` 14 | /* eslint-disable */ 15 | var define = false; 16 | var global={}; 17 | var process = {env: {}}; 18 | var isServer = false; 19 | `, 20 | plugins: [ 21 | resolve({ 22 | module: true, // Default: true 23 | jsnext: true, // Default: false 24 | main: true, // Default: true 25 | browser: true, 26 | extensions: [ '.js', '.json' ], // Default: ['.js'] 27 | }), 28 | builtins(), 29 | commonjs({ 30 | include: 'node_modules/**', 31 | }), 32 | json({ 33 | include: 'node_modules/**', 34 | }) 35 | ] 36 | }; 37 | -------------------------------------------------------------------------------- /static/fonts/SamsungOne/SamsungOne-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SamsungInternet/web-code/caf2401227497d00cf728d5ee3edcbd9972c0771/static/fonts/SamsungOne/SamsungOne-300.woff -------------------------------------------------------------------------------- /static/fonts/SamsungOne/SamsungOne-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SamsungInternet/web-code/caf2401227497d00cf728d5ee3edcbd9972c0771/static/fonts/SamsungOne/SamsungOne-400.woff -------------------------------------------------------------------------------- /static/fonts/SamsungOne/SamsungOne-600.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SamsungInternet/web-code/caf2401227497d00cf728d5ee3edcbd9972c0771/static/fonts/SamsungOne/SamsungOne-600.woff -------------------------------------------------------------------------------- /static/fonts/SamsungOne/SamsungOne-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SamsungInternet/web-code/caf2401227497d00cf728d5ee3edcbd9972c0771/static/fonts/SamsungOne/SamsungOne-700.woff -------------------------------------------------------------------------------- /static/fonts/SamsungOne/SamsungOne-800.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SamsungInternet/web-code/caf2401227497d00cf728d5ee3edcbd9972c0771/static/fonts/SamsungOne/SamsungOne-800.woff -------------------------------------------------------------------------------- /static/fonts/SamsungOne/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Samsung One'; 3 | font-style: normal; 4 | font-weight: 300; 5 | src: url('SamsungOne-300.woff'); 6 | } 7 | 8 | @font-face { 9 | font-family: 'Samsung One'; 10 | font-style: normal; 11 | font-weight: 400; 12 | src: url('SamsungOne-400.woff'); 13 | } 14 | 15 | @font-face { 16 | font-family: 'Samsung One'; 17 | font-style: normal; 18 | font-weight: 600; 19 | src: url('SamsungOne-600.woff'); 20 | } 21 | 22 | @font-face { 23 | font-family: 'Samsung One'; 24 | font-style: normal; 25 | font-weight: 700; 26 | src: url('SamsungOne-700.woff'); 27 | } 28 | 29 | @font-face { 30 | font-family: 'Samsung One'; 31 | font-style: normal; 32 | font-weight: 800; 33 | src: url('SamsungOne-800.woff'); 34 | } 35 | -------------------------------------------------------------------------------- /static/fonts/flottflott/Flottflott.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SamsungInternet/web-code/caf2401227497d00cf728d5ee3edcbd9972c0771/static/fonts/flottflott/Flottflott.ttf -------------------------------------------------------------------------------- /static/fonts/flottflott/OFL-FAQ.txt: -------------------------------------------------------------------------------- 1 | Eine kurze deutsche Zusammenfassung der OFL finden Sie auf: 2 | http://de.wikipedia.org/wiki/SIL_Open_Font_License 3 | 4 | 5 | 6 | OFL FAQ - Frequently Asked Questions about the SIL Open Font License (OFL) 7 | Version 1.1-update1 - 31 March 2009 8 | (See http://scripts.sil.org/OFL for updates) 9 | 10 | 11 | 1 ABOUT USING AND DISTRIBUTING FONTS LICENSED UNDER THE OFL 12 | 13 | 1.1 Can I use the fonts in any publication, even embedded in the file? 14 | Yes. You may use them like most other fonts, but unlike some fonts you may include an embedded subset of the fonts in your document. Such use does not require you to include this license or other files (listed in OFL condition 2), nor does it require any type of acknowledgement within the publication. Some mention of the font name within the publication information (such as in a colophon) is usually appreciated. If you wish to include the complete font as a separate file, you should distribute the full font package, including all existing acknowledgements, and comply with the OFL conditions. Of course, referencing or embedding an OFL font in any document does not change the license of the document itself. The requirement for fonts to remain under the OFL does not apply to any document created using the fonts and their derivatives. Similarly, creating any kind of graphic using a font under OFL does not make the resulting artwork subject to the OFL. 15 | 16 | 1.2 Can I make web pages using these fonts? 17 | Yes! Go ahead! Using CSS (Cascading Style Sheets) is recommended. Direct usage of fonts retrieved from a remote server - also referred to as font linking - using cross-platform open standards like @font-face is encouraged. This is considered to be use and distribution for which the OFL explicitly grants permission. The font file itself is not embedded in the webpage but referenced through a web address (i.e. a URI regardless of file format or access protocol) which will cause the browser to retrieve and use the corresponding font to render the webpage. This usage scenario is different from font embedding within a document (i.e. a PDF) where the font or some of its elements become part of the document. Note that embedding in a document is also permitted by the license as indicated in 1.1. (See 1.10 for details related to URL-based access restrictions methods or DRM mechanisms). 18 | 19 | 1.3 Can I make the fonts available to others from my web site? 20 | Yes, as long as you meet the conditions of the license (do not sell by itself, include the necessary files, include the necessary copyright and license information, rename Modified Versions, do not abuse the Author(s)' name(s) and do not sublicense). In the case where you are hosting fonts to be served on the web, make sure the file contains the needed copyright notice(s) and licensing information in its metadata. Please double-check the accuracy of every field to prevent contradictory information. If you are making the font available for use via the @font-face open standard, putting this information in the standard font metadata fields is sufficient. Other font formats, including EOT and proposed superior alternatives, already provide fields for this information. 21 | 22 | 1.4 Can the fonts be included with Free/Libre and Open Source Software collections such as GNU/Linux and BSD distributions? 23 | Yes! Fonts licensed under the OFL can be freely aggregated with software under FLOSS (Free/Libre and Open Source Software) licenses. Since fonts are much more useful aggregated to than merged with existing software, possible incompatibility with existing software licenses is not a problem. You can also repackage the fonts and the accompanying components in a .rpm or .deb package and include them in distro CD/DVDs and online repositories. 24 | 25 | 1.5 I want to distribute the fonts with my program. Does this mean my program also has to be free/libre and open source software? 26 | No. Only the portions based on the Font Software are required to be released under the OFL. The intent of the license is to allow aggregation or bundling with software under restricted licensing as well. 27 | 28 | 1.6 Can I include the fonts on a CD of freeware or commercial fonts? 29 | Yes, as long some other font or software is also on the disk, so the OFL font is not sold by itself. 30 | 31 | 1.7 Can I sell a software package that includes these fonts? 32 | Yes, you can do this with both the Original Version and a Modified Version. Examples of bundling made possible by the OFL would include: word processors, design and publishing applications, training and educational software, edutainment software, etc. 33 | 34 | 1.8 Why won't the OFL let me sell the fonts alone? 35 | The intent is to keep people from making money by simply redistributing the fonts. The only people who ought to profit directly from the fonts should be the original authors, and those authors have kindly given up potential direct income to distribute their fonts under the OFL. Please honor and respect their contribution! 36 | 37 | 1.9 I've come across a font released under the OFL. How can I easily get more information about the Original Version? How can I know where it stands compared to the Original Version or other Modified Versions? 38 | Consult the copyright statement(s) in the license for ways to contact the original authors. Consult the FONTLOG for information on how the font differs from the Original Version, and get in touch with the various contributors via the information in the acknowledgment section. Please consider using the Original Versions of the fonts whenever possible. 39 | 40 | 1.10 What do you mean in condition 4? Can you provide examples of abusive promotion / endorsement / advertisement vs. normal acknowledgement? 41 | The intent is that the goodwill and reputation of the author(s) should not be used in a way that makes it sound like the original author(s) endorse or approve of a specific Modified Version or software bundle. For example, it would not be right to advertise a word processor by naming the author(s) in a listing of software features, or to promote a Modified Version on a web site by saying "designed by ...". However, it would be appropriate to acknowledge the author(s) if your software package has a list of people who deserve thanks. We realize that this can seem to be a gray area, but the standard used to judge an acknowledgement is that if the acknowledgement benefits the author(s) it is allowed, but if it primarily benefits other parties, or could reflect poorly on the author(s), then it is not. 42 | 43 | 1.11 Can Font Software released under the OFL be subject to URL-based access restrictions methods or DRM mechanisms? 44 | Yes, but these issues are out-of-scope for the OFL. The license itself neither encourages their use nor prohibits them since such mechanisms are not implemented in the components of the Font Software but through external software. Such restrictions are put in place for many different purposes corresponding to various usage scenarios. One common example is to limit potentially dangerous cross-site scripting attacks. However, in the spirit of libre/open fonts and unrestricted writing systems, we strongly encourage open sharing and reuse of OFL fonts, and the establishment of an environment where such restrictions are unnecessary. Note that whether you wish to use such mechanisms or you prefer not to, you must still abide by the rules set forth by the OFL when using fonts released by their authors under this license. Derivative fonts must be licensed under the OFL, even if they are part of a service for which you charge fees and/or for which access to source code is restricted. You may not sell the fonts on their own - they must be part of a larger software package or bundle. For example, even if the OFL font is distributed in a software package or via an online service using a DRM mechanism, the user would still have the right to extract that font, use, study, modify and redistribute it under the OFL. 45 | 46 | 1.12 What about distributing fonts with a document? Within a compressed folder structure like an OpenDocument file (.odt) for example? Is it redistribution, bundling or embedding? 47 | The vast majority of the time, documents circulated in electronic form reference a font name which will match the corresponding font on the target system but do not carry the font within themselves. There may, however, be some cases where you need to bundle a font with the document. Certain document formats may allow the inclusion of an unmodified font within their file structure which consists of a compressed folder containing the various resources forming the document (such as pictures and thumbnails). Including fonts within such a structure is understood as being different from embedding but rather similar to bundling (or mere aggregation) for which the licensing makes explicit provision. In this case the font is conveyed unchanged whereas embedding a font transforms it from the original format. The OFL does not allow anyone to extract the font from such a structure to then redistribute it under another license. The explicit permission to redistribute and embed does not cancel the requirement for the Font Software to remain under the license chosen by its Author(s). 48 | 49 | 1.13 If OFL fonts are extracted from a document in which they are embedded (such as a PDF file), what can be done with them? Is this a risk to Author(s)? 50 | The few utilities that can extract fonts embedded in a PDF will only output limited amounts of outlines - not a complete font. To create a working font from this method is much more difficult than finding the source of the original OFL font. So there is little chance that an OFL font would be extracted and redistributed inappropriately through this method. Even so, copyright laws address any misrepresentation of authorship. All Font Software released under the OFL and marked as such by the Author(s) is intended to remain under this license regardless of the distribution method, and cannot be redistributed under any other license. We strongly discourage any font extraction - we recommend directly using the font sources instead - but if you extract font outlines from a document please be considerate, use your common sense and respect the work of the Author(s) and the licensing model. 51 | 52 | 1.14 What about sharing OFL fonts with friends on a CD, DVD or USB stick? 53 | You are very welcome to share open fonts with friends, family and colleagues on such removable media. Please make sure that you share and share-alike as much as possible from what the Author(s) released and that you don't strip away useful information which may not be present in the binary font files themselves. Just remember that in the case where you sell the font, it has to come bundled with software. 54 | 55 | 56 | 2 ABOUT MODIFYING OFL LICENSED FONTS 57 | 58 | 2.1 Can I change the fonts? Are there any limitations to what things I can and cannot change? 59 | You are allowed to change anything, as long as such changes do not violate the terms of the license. In other words, you are not allowed to remove the copyright statement(s) from the font, but you could add additional information into it that covers your contribution. 60 | 61 | 2.2 I have a font that needs a few extra glyphs - can I take them from an OFL licensed font and copy them into mine? 62 | Yes, but if you distribute that font to others it must be under the OFL, and include the information mentioned in condition 2 of the license. 63 | 64 | 2.3 Can I charge people for my additional work? In other words, if I add a bunch of special glyphs and/or OpenType/Graphite code, can I sell the enhanced font? 65 | Not by itself. Derivative fonts must be released under the OFL and cannot be sold by themselves. It is permitted, however, to include them in a larger software package (such as text editors, office suites or operating systems), even if the larger package is sold. In that case, you are strongly encouraged, but not required, to also make that derived font easily and freely available outside of the larger package. 66 | 67 | 2.4 Can I pay someone to enhance the fonts for my use and distribution? 68 | Yes. This is a good way to fund the further development of the fonts. Keep in mind, however, that if the font is distributed to others it must be under the OFL. You won't be able to recover your investment by exclusively selling the font, but you will be making a valuable contribution to the community. Please remember how you have benefitted from the contributions of others. 69 | 70 | 2.5 I need to make substantial revisions to the font to make it work with my program. It will be a lot of work, and a big investment, and I want to be sure that it can only be distributed with my program. Can I restrict its use? 71 | No. If you redistribute a Modified Version of the font it must be under the OFL. You may not restrict it in any way. This is intended to ensure that all released improvements to the fonts become available to everyone. But you will likely get an edge over competitors by being the first to distribute a bundle with the enhancements. Again, please remember how you have benefitted from the contributions of others. 72 | 73 | 2.6 Do I have to make any derivative fonts (including extended source files, build scripts, documentation, etc.) publicly available? 74 | No, but please do share your improvements with others. You may find that you receive more than what you gave in return. 75 | 76 | 2.7 Why can't I use the Reserved Font Name(s) in my derivative font names? I'd like people to know where the design came from. 77 | The best way to acknowledge the source of the design is to thank the original authors and any other contributors in the files that are distributed with your revised font (although no acknowledgement is required). The FONTLOG is a natural place to do this. Reserved Font Name(s) ensure that the only fonts that have the original names are the unmodified Original Versions. This allows designers to maintain artistic integrity while allowing collaboration to happen. It eliminates potential confusion and name conflicts. When choosing a name be creative and avoid names that reuse almost all the same letters in the same order or sound like the original. Keep in mind that the Copyright Holder(s) can allow a specific trusted partner to use Reserved Font Name(s) through a separate written agreement. 78 | 79 | 2.8 What do you mean by "primary name as presented to the user"? Are you referring to the font menu name? 80 | Yes, the requirement to change the visible name used to differentiate the font from others applies to the font menu name and other mechanisms to specify a font in a document. It would be fine, for example, to keep a text reference to the original fonts in the description field, in your modified source file or in documentation provided alongside your derivative as long as no one could be confused that your modified source is the original. But you cannot use the Reserved Font Names in any way to identify the font to the user (unless the Copyright Holder(s) allow(s) it through a separate agreement; see section 2.7). Users who install derivatives ("Modified Versions") on their systems should not see any of the original names ("Reserved Font Names") in their font menus, for example. Again, this is to ensure that users are not confused and do not mistake a font for another and so expect features only another derivative or the Original Version can actually offer. Ultimately, creating name conflicts will cause many problems for the users as well as for the designer of both the Original and Modified versions, so please think ahead and find a good name for your own derivative. Font substitution systems like fontconfig, or application-level font fallback configuration within OpenOffice.org or Scribus, will also get very confused if the name of the font they are configured to substitute to actually refers to another physical font on the user's hard drive. It will help everyone if Original Versions and Modified Versions can easily be distinguished from one another and from other derivatives. The substitution mechanism itself is outside the scope of the license. Users can always manually change a font reference in a document or set up some kind of substitution at a higher level but at the lower level the fonts themselves have to respect the Reserved Font Name(s) requirement to prevent ambiguity. If a substitution is currently active the user should be aware of it. 81 | 82 | 2.9 Am I not allowed to use any part of the Reserved Font Names? 83 | You may not use the words of the font names, but you would be allowed to use parts of words, as long as you do not use any word from the Reserved Font Names entirely. We do not recommend using parts of words because of potential confusion, but it is allowed. For example, if "Foobar" was a Reserved Font Name, you would be allowed to use "Foo" or "bar", although we would not recommend it. Such an unfortunate choice would confuse the users of your fonts as well as make it harder for other designers to contribute. 84 | 85 | 2.10 So what should I, as an author, identify as Reserved Font Names? 86 | Original authors are encouraged to name their fonts using clear, distinct names, and only declare the unique parts of the name as Reserved Font Names. For example, the author of a font called "Foobar Sans" would declare "Foobar" as a Reserved Font Name, but not "Sans", as that is a common typographical term, and may be a useful word to use in a derivative font name. Reserved Font Names should also be single words. A font called "Flowing River" should have Reserved Font Names "Flowing" and "River", not "Flowing River". 87 | 88 | 2.11 Do I, as an author, have to identify any Reserved Font Names? 89 | No, but we strongly encourage you to do so. This is to avoid confusion between your work and Modified versions. You may, however, give certain trusted parties the right to use any of your Reserved Font Names through separate written agreements. For example, even if "Foobar" is a RFN, you could write up an agreement to give company "XYZ" the right to distribute a modified version with a name that includes "Foobar". This allows for freedom without confusion. 90 | 91 | 2.12 Are any names (such as the main font name) reserved by default? 92 | No. That is a change to the license as of version 1.1. If you want any names to be Reserved Font Names, they must be specified after the copyright statement(s). 93 | 94 | 2.13 What is this FONTLOG thing exactly? 95 | It has three purposes: 1) to provide basic information on the font to users and other developers, 2) to document changes that have been made to the font or accompanying files, either by the original authors or others, and 3) to provide a place to acknowledge the authors and other contributors. Please use it! See below for details on how changes should be noted. 96 | 97 | 2.14 Am I required to update the FONTLOG? 98 | No, but users, designers and other developers might get very frustrated at you if you don't! People need to know how derivative fonts differ from the original, and how to take advantage of the changes, or build on them. 99 | 100 | 101 | 3 ABOUT THE FONTLOG 102 | 103 | The FONTLOG can take a variety of formats, but should include these four sections: 104 | 105 | 3.1 FONTLOG for 106 | This file provides detailed information on the Font Software. This information should be distributed along with the fonts and any derivative works. 107 | 108 | 3.2 Basic Font Information 109 | (Here is where you would describe the purpose and brief specifications for the font project, and where users can find more detailed documentation. It can also include references to how changes can be contributed back to the Original Version. You may also wish to include a short guide to the design, or a reference to such a document.) 110 | 111 | 3.3 ChangeLog 112 | (This should list both major and minor changes, most recent first. Here are some examples:) 113 | 114 | 7 February 2007 (Pat Johnson) Version 1.3 115 | - Added Greek and Cyrillic glyphs 116 | - Released as "" 117 | 118 | 7 March 2006 (Fred Foobar) Version 1.2 119 | - Tweaked contextual behaviours 120 | - Released as "" 121 | 122 | 1 Feb 2005 (Jane Doe) Version 1.1 123 | - Improved build script performance and verbosity 124 | - Extended the smart code documentation 125 | - Corrected minor typos in the documentation 126 | - Fixed position of combining inverted breve below (U+032F) 127 | - Added OpenType/Graphite smart code for Armenian 128 | - Added Armenian glyphs (U+0531 -> U+0587) 129 | - Released as "" 130 | 131 | 1 Jan 2005 (Joe Smith) Version 1.0 132 | - Initial release of font "" 133 | 134 | 3.4 Acknowledgements 135 | (Here is where contributors can be acknowledged. 136 | 137 | If you make modifications be sure to add your name (N), email (E), web-address (W) and description (D). This list is sorted by last name in alphabetical order.) 138 | 139 | N: Jane Doe 140 | E: jane@university.edu 141 | W: http://art.university.edu/projects/fonts 142 | D: Contributor - Armenian glyphs and code 143 | 144 | N: Fred Foobar 145 | E: fred@foobar.org 146 | W: http://foobar.org 147 | D: Contributor - misc Graphite fixes 148 | 149 | N: Pat Johnson 150 | E: pat@fontstudio.org 151 | W: http://pat.fontstudio.org 152 | D: Designer - Greek & Cyrillic glyphs based on Roman design 153 | 154 | N: Tom Parker 155 | E: tom@company.com 156 | W: http://www.company.com/tom/projects/fonts 157 | D: Engineer - original smart font code 158 | 159 | N: Joe Smith 160 | E: joe@fontstudio.org 161 | W: http://joe.fontstudio.org 162 | D: Designer - original Roman glyphs 163 | 164 | (Original authors can also include information here about their organization.) 165 | 166 | 167 | 4 ABOUT MAKING CONTRIBUTIONS 168 | 169 | 4.1 Why should I contribute my changes back to the original authors? 170 | It would benefit many people if you contributed back to what you've received. Providing your contributions and improvements to the fonts and other components (data files, source code, build scripts, documentation, etc.) could be a tremendous help and would encourage others to contribute as well and 'give back', which means you will have an opportunity to benefit from other people's contributions as well. Sometimes maintaining your own separate version takes more effort than merging back with the original. Be aware that any contributions, however, must be either your own original creation or work that you own, and you may be asked to affirm that clearly when you contribute. 171 | 172 | 4.2 I've made some very nice improvements to the font, will you consider adopting them and putting them into future Original Versions? 173 | Most authors would be very happy to receive such contributions. Keep in mind that it is unlikely that they would want to incorporate major changes that would require additional work on their end. Any contributions would likely need to be made for all the fonts in a family and match the overall design and style. Authors are encouraged to include a guide to the design with the fonts. It would also help to have contributions submitted as patches or clearly marked changes (the use of smart source revision control systems like subversion, svk, mercurial, git or bzr is a good idea). Examples of useful contributions are bug fixes, additional glyphs, stylistic alternates (and the smart font code to access them) or improved hinting. 174 | 175 | 4.3 How can I financially support the development of OFL fonts? 176 | It is likely that most authors of OFL fonts would accept financial contributions - contact them for instructions on how to do this. Such contributions would support future development. You can also pay for others to enhance the fonts and contribute the results back to the original authors for inclusion in the Original Version. 177 | 178 | (In case of the font, that comes with this copy of the FAQ-file, you will find a paypal(tm) donation link on the fonts-page of www.peter-wiegel.de) 179 | 180 | 181 | 5 ABOUT THE LICENSE 182 | 183 | 5.1 I see that this is version 1.1 of the license. Will there be later changes? 184 | Version 1.1 is the first minor revision of the OFL. We are confident that version 1.1 will meet most needs, but are open to future improvements. Any revisions would be for future font releases, and previously existing licenses would remain in effect. No retroactive changes are possible, although the Copyright Holder(s) can re-release the font under a revised OFL. All versions will be available on our web site: http://scripts.sil.org/OFL. 185 | 186 | 5.2 Can I use the SIL Open Font License for my own fonts? 187 | Yes! We heartily encourage anyone to use the OFL to distribute their own original fonts. It is a carefully constructed license that allows great freedom along with enough artistic integrity protection for the work of the authors as well as clear rules for other contributors and those who redistribute the fonts. Some additional information about using the OFL is included at the end of this FAQ. 188 | 189 | 5.3 Does this license restrict the rights of the Copyright Holder(s)? 190 | No. The Copyright Holder(s) still retain(s) all the rights to their creation; they are only releasing a portion of it for use in a specific way. For example, the Copyright Holder(s) may choose to release a 'basic' version of their font under the OFL, but sell a restricted 'enhanced' version. Only the Copyright Holder(s) can do this. 191 | 192 | 5.4 Is the OFL a contract or a license? 193 | The OFL is a license and not a contract and so does not require you to sign it to have legal validity. By using, modifying and redistributing components under the OFL you indicate that you accept the license. 194 | 195 | 5.5 How about translating the license and the FAQ into other languages? 196 | SIL certainly recognises the need for people who are not familiar with English to be able to understand the OFL and this FAQ better in their own language. Making the license very clear and readable is a key goal of the OFL. 197 | 198 | If you are an experienced translator, you are very welcome to help by translating the OFL and its FAQ so that designers and users in your language community can understand the license better. But only the original English version of the license has legal value and has been approved by the community. Translations do not count as legal substitutes and should only serve as a way to explain the original license. SIL - as the author and steward of the license for the community at large - does not approve any translation of the OFL as legally valid because even small translation ambiguities could be abused and create problems. 199 | 200 | We give permission to publish unofficial translations into other languages provided that they comply with the following guidelines: 201 | 202 | - put the following disclaimer in both English and the target language stating clearly that the translation is unofficial: 203 | 204 | "This is an unofficial translation of the SIL Open Font License into $language. It was not published by SIL International, and does not legally state the distribution terms for fonts that use the OFL. A release under the OFL is only valid when using the original English text. 205 | 206 | However, we recognize that this unofficial translation will help users and designers not familiar with English to understand the SIL OFL better and make it easier to use and release font families under this collaborative font design model. We encourage designers who consider releasing their creation under the OFL to read the FAQ in their own language if it is available. 207 | 208 | Please go to http://scripts.sil.org/OFL for the official version of the license and the accompanying FAQ." 209 | 210 | - keep your unofficial translation current and update it at our request if needed, for example if there is any ambiguity which could lead to confusion. 211 | 212 | If you start such a unofficial translation effort of the OFL and its accompanying FAQ please let us know, thank you. 213 | 214 | 215 | 6 ABOUT SIL INTERNATIONAL 216 | 217 | 6.1 Who is SIL International and what does it do? 218 | SIL International is a worldwide faith-based education and development organization (NGO) that studies, documents, and assists in developing the world's lesser-known languages through literacy, linguistics, translation, and other academic disciplines. SIL makes its services available to all without regard to religious belief, political ideology, gender, race, or ethnic background. SIL's members and volunteers share a Christian commitment. 219 | 220 | 6.2 What does this have to do with font licensing? 221 | The ability to read, write, type and publish in one's own language is one of the most critical needs for millions of people around the world. This requires fonts that are widely available and support lesser-known languages. SIL develops - and encourages others to develop - a complete stack of writing systems implementation components available under open licenses. This open stack includes input methods, smart fonts, smart rendering libraries and smart applications. There has been a need for a common open license that is specifically applicable to fonts and related software (a crucial component of this stack) so SIL developed the SIL Open Font License with the help of the FLOSS community. 222 | 223 | 6.3 How can I contact SIL? 224 | Our main web site is: http://www.sil.org/ 225 | Our site about complex scripts is: http://scripts.sil.org/ 226 | Information about this license (including contact email information) is at: http://scripts.sil.org/OFL 227 | 228 | 229 | 7 ABOUT USING THE OFL FOR YOUR ORIGINAL FONTS 230 | 231 | If you want to release your fonts under the OFL, you only need to do the following: 232 | 233 | 7.1 Put your copyright and reserved font names information in the beginning of the main OFL file (simply use the dedicated placeholders). 234 | 7.2 Put your copyright and the OFL references in your various font files (such as in the copyright, license and description fields) and in your other components (build scripts, glyph databases, documentation, rendering samples, etc). Accurate metadata in your font files is beneficial to you as an increasing number of applications are exposing this information to the user. For example, clickable links can bring users back to your website and let them know about other work you have done or services you provide. Depending on the format of your fonts and sources, you can use template human-readable headers or machine-readable metadata. 235 | 7.3 Write an initial FONTLOG for your font and include it in the release package. 236 | 7.4 Include the OFL license file in your release package. 237 | 7.5 We also highly recommend you include the relevant practical documentation on the license by putting the OFL-FAQ in your package. 238 | 7.6 If you wish, you can use the OFL Graphics on your web page. 239 | 240 | That's all. If you have any more questions please get in touch with us. 241 | 242 | 243 | -------------------------------------------------------------------------------- /static/fonts/flottflott/Open Font License.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Peter Wiegel, www.peter-wiegel.de, wiegel@peter-wiegel.de 2 | with Reserved Font Name Flottflott. 3 | 4 | 5 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 6 | This license is copied below, and is also available with a FAQ at: 7 | http://scripts.sil.org/OFL 8 | 9 | 10 | ----------------------------------------------------------- 11 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 12 | ----------------------------------------------------------- 13 | 14 | PREAMBLE 15 | The goals of the Open Font License (OFL) are to stimulate worldwide 16 | development of collaborative font projects, to support the font creation 17 | efforts of academic and linguistic communities, and to provide a free and 18 | open framework in which fonts may be shared and improved in partnership 19 | with others. 20 | 21 | The OFL allows the licensed fonts to be used, studied, modified and 22 | redistributed freely as long as they are not sold by themselves. The 23 | fonts, including any derivative works, can be bundled, embedded, 24 | redistributed and/or sold with any software provided that any reserved 25 | names are not used by derivative works. The fonts and derivatives, 26 | however, cannot be released under any other type of license. The 27 | requirement for fonts to remain under this license does not apply 28 | to any document created using the fonts or their derivatives. 29 | 30 | DEFINITIONS 31 | "Font Software" refers to the set of files released by the Copyright 32 | Holder(s) under this license and clearly marked as such. This may 33 | include source files, build scripts and documentation. 34 | 35 | "Reserved Font Name" refers to any names specified as such after the 36 | copyright statement(s). 37 | 38 | "Original Version" refers to the collection of Font Software components as 39 | distributed by the Copyright Holder(s). 40 | 41 | "Modified Version" refers to any derivative made by adding to, deleting, 42 | or substituting -- in part or in whole -- any of the components of the 43 | Original Version, by changing formats or by porting the Font Software to a 44 | new environment. 45 | 46 | "Author" refers to any designer, engineer, programmer, technical 47 | writer or other person who contributed to the Font Software. 48 | 49 | PERMISSION & CONDITIONS 50 | Permission is hereby granted, free of charge, to any person obtaining 51 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 52 | redistribute, and sell modified and unmodified copies of the Font 53 | Software, subject to the following conditions: 54 | 55 | 1) Neither the Font Software nor any of its individual components, 56 | in Original or Modified Versions, may be sold by itself. 57 | 58 | 2) Original or Modified Versions of the Font Software may be bundled, 59 | redistributed and/or sold with any software, provided that each copy 60 | contains the above copyright notice and this license. These can be 61 | included either as stand-alone text files, human-readable headers or 62 | in the appropriate machine-readable metadata fields within text or 63 | binary files as long as those fields can be easily viewed by the user. 64 | 65 | 3) No Modified Version of the Font Software may use the Reserved Font 66 | Name(s) unless explicit written permission is granted by the corresponding 67 | Copyright Holder. This restriction only applies to the primary font name as 68 | presented to the users. 69 | 70 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 71 | Software shall not be used to promote, endorse or advertise any 72 | Modified Version, except to acknowledge the contribution(s) of the 73 | Copyright Holder(s) and the Author(s) or with their explicit written 74 | permission. 75 | 76 | 5) The Font Software, modified or unmodified, in part or in whole, 77 | must be distributed entirely under this license, and must not be 78 | distributed under any other license. The requirement for fonts to 79 | remain under this license does not apply to any document created 80 | using the Font Software. 81 | 82 | TERMINATION 83 | This license becomes null and void if any of the above conditions are 84 | not met. 85 | 86 | DISCLAIMER 87 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 88 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 89 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 90 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 91 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 92 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 93 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 94 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 95 | OTHER DEALINGS IN THE FONT SOFTWARE. 96 | -------------------------------------------------------------------------------- /static/fonts/flottflott/fonts.css: -------------------------------------------------------------------------------- 1 | 2 | @font-face { 3 | font-family: 'flottflott'; 4 | src: url('Flottflott.ttf'); 5 | } -------------------------------------------------------------------------------- /static/icon192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SamsungInternet/web-code/caf2401227497d00cf728d5ee3edcbd9972c0771/static/icon192.png -------------------------------------------------------------------------------- /static/icon512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SamsungInternet/web-code/caf2401227497d00cf728d5ee3edcbd9972c0771/static/icon512.png -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Editor 33 | 34 | 35 |
36 |

Web Code

37 | 38 | 39 | 40 |
41 |
42 | 46 |
47 |
48 |
    49 |
50 |
51 |
52 |
53 | 54 |
55 |
56 | 57 | 58 |
59 |
60 | 61 | 62 |
63 |
64 |
    65 |
      66 |
      67 |
      68 | 69 | 70 |
      71 |
      72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Web Code", 3 | "short_name": "Web Code", 4 | "lang": "en-GB", 5 | "start_url": "/", 6 | "display": "standalone", 7 | "theme_color": "#333333", 8 | "background_color": "#333333", 9 | "icons": [ 10 | { 11 | "src": "icon192.png", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "icon512.png", 17 | "sizes": "512x512", 18 | "type": "image/png" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /static/scripts/lib/buffer-file.js: -------------------------------------------------------------------------------- 1 | /* Like web-code-stats but for storing the file inside and not as a seperate file */ 2 | /* eslint no-var: 0, no-console: 0 */ 3 | /* eslint-env es6 */ 4 | 5 | import {updateDBDoc, db} from './db.js'; 6 | 7 | var idToBufferFileMap = new Map(); 8 | 9 | var compulsaryAttrs = [ 10 | 'name', 11 | 'id', 12 | 'icon' 13 | ]; 14 | 15 | var optionalAttrs = [ 16 | 'mime' 17 | ] 18 | 19 | var keys = compulsaryAttrs.concat(optionalAttrs); 20 | 21 | export default function BufferFile(data) { 22 | 23 | this.data = {}; 24 | 25 | compulsaryAttrs.forEach(function (key) { 26 | if (data[key]) { 27 | this.data[key] = data[key]; 28 | } else { 29 | throw Error('Missing Key: ' + key); 30 | } 31 | }.bind(this)); 32 | 33 | if (idToBufferFileMap.has(data.id)) { 34 | return idToBufferFileMap.get(data.id); 35 | } 36 | idToBufferFileMap.set(data.id, this) 37 | 38 | optionalAttrs.forEach(function (key) { 39 | if (data[key]) { 40 | this.data[key] = data[key]; 41 | } 42 | }.bind(this)); 43 | 44 | // Try fetching from DB 45 | this.valuePromise = db.get(data.id) 46 | .catch(function (e) { 47 | if (e.status === 404) { 48 | var doc = this.toDoc(); 49 | doc._id = this.data.id; 50 | doc.value = ''; 51 | return db.put(doc).then(function () { 52 | return doc; 53 | }); 54 | } 55 | throw e; 56 | }.bind(this)) 57 | .then(function (doc) { 58 | this.value = doc.value; 59 | return doc.value; 60 | }.bind(this)); 61 | } 62 | 63 | BufferFile.prototype.update = function update(value) { 64 | // save doc to disk 65 | return this.valuePromise = this.valuePromise 66 | .then(function () { 67 | return updateDBDoc(this.data.id, { 68 | value: value 69 | }); 70 | }.bind(this)) 71 | .then(function () { 72 | return value; 73 | }); 74 | } 75 | 76 | BufferFile.prototype.toDoc = function toDoc() { 77 | var out = { 78 | isBufferFileDoc: true 79 | }; 80 | keys.forEach(function (key) { 81 | out[key] = this.data[key]; 82 | }.bind(this)); 83 | return out; 84 | } -------------------------------------------------------------------------------- /static/scripts/lib/db.js: -------------------------------------------------------------------------------- 1 | /* global Promise, isServer */ 2 | /* eslint no-var: 0, no-console: 0 */ 3 | /* eslint-env es6 */ 4 | 5 | import PouchDB from 'pouchdb-browser'; 6 | try { 7 | var db = isServer ? null : new PouchDB('web-code', {}); 8 | } catch (e) { 9 | console.log(e); 10 | } 11 | function updateDBDoc(_id, obj) { 12 | 13 | updateDBDoc.promise = updateDBDoc.promise || Promise.resolve(); 14 | 15 | /* update last open folder in db */ 16 | return updateDBDoc.promise = updateDBDoc.promise 17 | .then(function () { 18 | return db.get(_id) 19 | }) 20 | .catch(function (e) { 21 | if (e.status === 404) { 22 | return { _id: _id } 23 | } 24 | if (e.name === 'indexed_db_went_bad') { 25 | console.log('Updating DB Failed: ' + e.reason); 26 | return; 27 | } 28 | throw e; 29 | }) 30 | .then(function (doc) { 31 | if (!doc) return; 32 | Object.keys(obj).forEach(function (key) { 33 | doc[key] = obj[key]; 34 | }); 35 | db.put(doc); 36 | }); 37 | } 38 | 39 | export { db, updateDBDoc }; -------------------------------------------------------------------------------- /static/scripts/lib/errors.js: -------------------------------------------------------------------------------- 1 | /* eslint no-var: 0, no-console: 0 */ 2 | 3 | function displayError(type, text, timeout) { 4 | 5 | var errorEl = document.getElementById('errors'); 6 | 7 | var li = document.createElement('li'); 8 | 9 | var textEl = document.createElement('span'); 10 | textEl.classList.add('error-text'); 11 | textEl.textContent = text; 12 | 13 | var typeEl = document.createElement('span'); 14 | typeEl.classList.add('error-type'); 15 | typeEl.textContent = type; 16 | 17 | li.appendChild(typeEl); 18 | li.appendChild(textEl); 19 | 20 | if (timeout) { 21 | setTimeout(function () { 22 | errorEl.removeChild(li); 23 | }, timeout); 24 | } 25 | 26 | errorEl.appendChild(li); 27 | return li; 28 | } 29 | 30 | function removeError(el) { 31 | var errorEl = document.getElementById('errors'); 32 | errorEl.removeChild(el); 33 | } 34 | 35 | export { 36 | removeError, 37 | displayError 38 | } 39 | 40 | -------------------------------------------------------------------------------- /static/scripts/lib/file-dialog.js: -------------------------------------------------------------------------------- 1 | /* global Map, Set, Promise */ 2 | /* eslint no-var: 0, no-console: 0 */ 3 | /* eslint-env es6 */ 4 | 5 | /* NEEDS REFACTORING */ 6 | 7 | import { populateFileList, destroyFileList } from './files'; 8 | import Stats from './web-code-stats'; 9 | import { resolve, join } from 'path'; 10 | 11 | var highlightedEl; 12 | var currentPath; 13 | var resolver; 14 | var rejecter; 15 | 16 | function fileDialog(options) { 17 | 18 | return new Promise(function (resolve, reject) { 19 | 20 | var role; 21 | var path = options.path || process.env.HOME || '/'; 22 | 23 | if (fileDialog.open === undefined) fileDialog.open = false; 24 | if (fileDialog.open === true) { 25 | throw Error('Dialog already open for another task.'); 26 | } 27 | 28 | fileDialog.el.classList.remove('closed'); 29 | 30 | if (typeof options !== 'object') { 31 | throw Error('Invalid options object') 32 | } 33 | 34 | if (!options.role) { 35 | throw Error('Role not defined'); 36 | } 37 | 38 | if (options.role.toLowerCase() === 'open') { 39 | role = 'open'; 40 | fileDialog.submitButton.textContent = 'Open'; 41 | setTimeout(function () { 42 | fileDialog.filelistLeft.focus(); 43 | }, 300); 44 | } else if (options.role.toLowerCase() === 'save as') { 45 | role = 'save-as'; 46 | fileDialog.submitButton.textContent = 'Save'; 47 | setTimeout(function () { 48 | fileDialog.filename.focus(); 49 | }, 300); 50 | } else { 51 | throw Error('Unrecognised role, ' + options.role); 52 | } 53 | 54 | if (options.filename) { 55 | fileDialog.filename.value = options.filename; 56 | } 57 | 58 | fileDialog.el.dataset.role = role; 59 | 60 | currentPath = path; 61 | resolver = resolve; 62 | rejecter = reject; 63 | 64 | fileDialog.currentPathEl.value = currentPath; 65 | 66 | populateFileList(fileDialog.filelistLeft, path, { 67 | nested: false 68 | }) 69 | .catch(function (e) { 70 | console.log(e); 71 | return populateFileList(fileDialog.filelistLeft, process.env.HOME || '/', { 72 | nested: false 73 | }) 74 | }); 75 | }); 76 | } 77 | 78 | function highlight(e) { 79 | if (e.target.tagName === 'LI') { 80 | if (highlightedEl) { 81 | highlightedEl.classList.remove('has-highlight'); 82 | } 83 | highlightedEl = e.target; 84 | highlightedEl.classList.add('has-highlight'); 85 | 86 | currentPath = e.target.stats.data.path; 87 | 88 | 89 | if (e.target.stats && e.target.stats.isDirectory()) { 90 | fileDialog.currentPathEl.value = currentPath; 91 | if (e.currentTarget === fileDialog.filelistLeft) { 92 | if (e.target.stats.data.name === '..') { 93 | populateFileList(fileDialog.filelistLeft, e.target.stats.data.path, { 94 | nested: false 95 | }); 96 | destroyFileList(fileDialog.filelistRight); 97 | } else { 98 | populateFileList(fileDialog.filelistRight, e.target.stats.data.path, { 99 | nested: false 100 | }); 101 | } 102 | } 103 | if (e.currentTarget === fileDialog.filelistRight) { 104 | populateFileList(fileDialog.filelistLeft, e.target.stats.data.dirName, { 105 | nested: false 106 | }) 107 | .then(function () { 108 | [].slice.call(fileDialog.filelistLeft.children).forEach(function (el) { 109 | if (el.stats.data.path === currentPath) { 110 | highlightedEl = e.target; 111 | highlightedEl.classList.add('has-highlight'); 112 | } 113 | }); 114 | }); 115 | 116 | populateFileList(fileDialog.filelistRight, e.target.stats.data.path, { 117 | nested: false 118 | }); 119 | } 120 | } 121 | 122 | if (e.target.stats && e.target.stats.isFile()) { 123 | if (fileDialog.el.dataset.role === 'open') { 124 | fileDialog.currentPathEl.value = currentPath; 125 | } else { 126 | fileDialog.currentPathEl.value = e.target.stats.data.dirName; 127 | } 128 | fileDialog.filename.value = e.target.stats.data.name; 129 | } 130 | } 131 | } 132 | 133 | function ondblclick(e) { 134 | highlight(e); 135 | if (e.target.stats && e.target.stats.isDirectory()) return; 136 | submit(e.target.stats); 137 | } 138 | 139 | function submit(stats) { 140 | if (fileDialog.el.dataset.role === 'open') { 141 | resolver(stats); 142 | } 143 | if (fileDialog.el.dataset.role === 'save-as') { 144 | resolver(join(stats.isDirectory() ? stats.data.path : stats.data.dirName, fileDialog.filename.value)); 145 | } 146 | fileDialog.el.classList.add('closed'); 147 | resolver = undefined; 148 | rejecter = undefined; 149 | } 150 | 151 | function cancel() { 152 | fileDialog.el.classList.add('closed'); 153 | rejecter('User canceled'); 154 | resolver = undefined; 155 | rejecter = undefined; 156 | } 157 | 158 | function onkeydown(e) { 159 | if (event.keyCode === 13) ondblclick(e); 160 | e.stopPropagation(); 161 | } 162 | 163 | function setDialogPath(path) { 164 | fileDialog.currentPathEl.value = path; 165 | populateFileList(fileDialog.filelistLeft, path, { 166 | nested: false 167 | }); 168 | destroyFileList(fileDialog.filelistRight); 169 | } 170 | 171 | fileDialog.el = fileDialog.el || document.querySelector('#file-dialog-widget'); 172 | fileDialog.currentPathEl = fileDialog.currentPathEl || fileDialog.el.querySelector('input[name="current-path"]'); 173 | fileDialog.filelistLeft = fileDialog.filelistLeft || fileDialog.el.querySelector('.filelist:first-child'); 174 | fileDialog.filelistRight = fileDialog.filelistRight || fileDialog.el.querySelector('.filelist:not(:first-child)'); 175 | fileDialog.submitButton = fileDialog.submitButton || fileDialog.el.querySelector('#file-dialog-submit'); 176 | fileDialog.filename = fileDialog.filename || fileDialog.el.querySelector('#save-file-name'); 177 | fileDialog.cancelButton = fileDialog.cancelButton || fileDialog.el.querySelector('#file-dialog-cancel'); 178 | fileDialog.upDirButton = fileDialog.upDirButton || fileDialog.el.querySelector('button[data-action="up-dir"]'); 179 | 180 | fileDialog.el.addEventListener('keydown', function () { 181 | if (event.keyCode === 13) return Stats.fromPath(fileDialog.currentPathEl.value).then(submit); 182 | }); 183 | 184 | fileDialog.filelistLeft.addEventListener('click', highlight); 185 | fileDialog.filelistRight.addEventListener('click', highlight); 186 | 187 | fileDialog.filelistLeft.addEventListener('keydown', onkeydown); 188 | fileDialog.filelistRight.addEventListener('keydown', onkeydown); 189 | 190 | fileDialog.filelistLeft.addEventListener('dblclick', ondblclick); 191 | fileDialog.filelistRight.addEventListener('dblclick', ondblclick); 192 | fileDialog.submitButton.addEventListener('click', function () { 193 | return Stats.fromPath(fileDialog.currentPathEl.value).then(submit); 194 | }); 195 | fileDialog.cancelButton.addEventListener('click', function () { 196 | cancel(); 197 | }); 198 | fileDialog.upDirButton.addEventListener('click', function () { 199 | var path = resolve(join(fileDialog.currentPathEl.value, '/..')); 200 | setDialogPath(path); 201 | }); 202 | 203 | export default fileDialog; -------------------------------------------------------------------------------- /static/scripts/lib/files.js: -------------------------------------------------------------------------------- 1 | /* global Map, Set, Promise, monaco */ 2 | /* eslint no-var: 0, no-console: 0 */ 3 | /* eslint-env es6 */ 4 | 5 | import fs from './fs-proxy.js'; 6 | import Stats from './web-code-stats.js'; 7 | import BufferFile from './buffer-file.js'; 8 | import state from './state.js'; 9 | import { db, updateDBDoc } from './db.js'; 10 | import { tabController } from './tab-controller.js'; 11 | import { monacoPromise, getMonacoLanguageFromExtensions, getMonacoLanguageFromMimes, addBindings, monacoSettings } from './monaco.js'; 12 | import fileDialog from './file-dialog.js'; 13 | 14 | function populateFileList(el, path, options) { 15 | el.path = path; 16 | return Stats.fromPath(path) 17 | .then(function (stats) { 18 | if (stats.isFile()) { 19 | return Stats.fromPath(stats.data.dirName); 20 | } 21 | return stats; 22 | }) 23 | .then(function (stats) { 24 | 25 | // Teardown old file list if one is present 26 | if (el.stats) { 27 | el.stats.destroyFileList(el); 28 | } 29 | 30 | // set up new one 31 | stats.renderFileList(el, options); 32 | 33 | // Update the filelist from the server 34 | return stats.updateChildren(); 35 | }); 36 | } 37 | 38 | function destroyFileList(el) { 39 | if (el.stats) { 40 | el.stats.destroyFileList(el); 41 | } 42 | } 43 | 44 | function openPath(stats) { 45 | if (stats.isDirectory()) { 46 | 47 | if (state.get('currentlyOpenedPath') !== stats.data.path) { 48 | tabController.closeAll(); 49 | 50 | // Then open the saved tabs from last time 51 | db.get('OPEN_TABS_FOR_' + stats.data.path).then(function (tabs) { 52 | Promise.all(tabs.open_tabs.map(function (obj) { 53 | if (obj.__webStatDoc) { 54 | return Stats.fromPath(obj.path) 55 | .catch(function (e) { 56 | console.log(e.message); 57 | return null; 58 | }); 59 | } 60 | if (obj.isBufferFileDoc) { 61 | return new BufferFile(obj); 62 | } 63 | return null; 64 | })).then(function (statsArray) { 65 | statsArray.filter(function (a) { 66 | return a !== null; 67 | }).forEach(function (stats) { 68 | openFile(stats); 69 | }); 70 | }); 71 | }).catch(function (e) { 72 | console.log(e); 73 | }); 74 | } 75 | 76 | state.set('currentlyOpenedPath', stats.data.path); 77 | state.sync(); 78 | 79 | var filelist = document.getElementById('directory'); 80 | populateFileList(filelist, stats.data.path, { 81 | hideDotFiles: true 82 | }) 83 | .catch(function (e) { 84 | throw e; 85 | }); 86 | 87 | updateDBDoc('INIT_STATE', { 88 | previous_path: { path: stats.data.path } 89 | }) 90 | .catch(function (err) { 91 | console.log(err); 92 | }); 93 | 94 | } 95 | if (stats.isFile()) { 96 | openFile(stats); 97 | } 98 | } 99 | 100 | /** 101 | * returns a promise which resolves a Tab 102 | * 103 | * @param {Stats|FileBuffer} stats 104 | */ 105 | function openFile(stats) { 106 | 107 | if (tabController.hasTab(stats)) { 108 | tabController.focusTab(stats); 109 | } else { 110 | var newTab = tabController.newTab(stats); 111 | tabController.focusTab(newTab); 112 | 113 | if (stats.constructor === Stats) return monacoPromise 114 | .then(function () { 115 | if (stats.data.mime.match(/^image\//)) { 116 | var image = document.createElement('img'); 117 | image.src = '/api/imageproxy?url=' + encodeURIComponent(stats.data.path); 118 | newTab.contentEl.appendChild(image); 119 | newTab.contentEl.classList.add('image-container'); 120 | return newTab; 121 | } else if (stats.data.extension !== '.ts' && stats.data.mime.match(/^video\//)) { 122 | var video = document.createElement('video'); 123 | video.src = '/api/imageproxy?url=' + encodeURIComponent(stats.data.path); 124 | newTab.contentEl.appendChild(video); 125 | video.controls = true; 126 | newTab.contentEl.classList.add('image-container'); 127 | return newTab; 128 | } else { 129 | return fs.readFile(stats.data.path, 'utf8') 130 | .then(function (fileContents) { 131 | var language = getMonacoLanguageFromMimes(stats.data.mime) || getMonacoLanguageFromExtensions(stats.data.extension); 132 | newTab.editor = monaco.editor.create(newTab.contentEl, monacoSettings({ 133 | value: fileContents, 134 | language: language 135 | })); 136 | addBindings(newTab.editor, newTab); 137 | return newTab; 138 | }); 139 | } 140 | }) 141 | .catch(function (e) { 142 | console.log(e.message); 143 | }); 144 | 145 | if (stats.constructor === BufferFile) { 146 | return Promise.all([monacoPromise, stats.valuePromise]).then(function (arr) { 147 | return arr[1]; 148 | }) 149 | .then(function (value) { 150 | var language = getMonacoLanguageFromMimes(stats.data.mime) || getMonacoLanguageFromExtensions(stats.data.extension); 151 | newTab.editor = monaco.editor.create(newTab.contentEl, monacoSettings({ 152 | value: value, 153 | language: language 154 | })); 155 | addBindings(newTab.editor, newTab); 156 | return newTab; 157 | }); 158 | } 159 | } 160 | } 161 | 162 | function promptForOpen() { 163 | return fileDialog({ 164 | path: state.get('currentlyOpenedPath') || process.env.HOME || '/', 165 | role: 'open' 166 | }).then(openPath); 167 | } 168 | 169 | function smartOpen(path) { 170 | console.log('Trying to open, ' + path); 171 | fs.stat(path) 172 | .then(function (result) { 173 | if (result.isDirectory()) { 174 | return Stats.fromPath(path).then(function (stats) {openPath(stats)}); 175 | } 176 | if (result.isFile()) { 177 | return Stats.fromPath(path).then(function (stats) {openFile(stats)}); 178 | } 179 | }); 180 | } 181 | 182 | 183 | // Saves file and updates versions for changes 184 | function saveTextFileFromEditor(stats, editor) { 185 | if (stats.constructor === Stats) { 186 | var altId = editor.id; 187 | return fs.writeFile(stats.data.path, editor.getValue()) 188 | .then(function () { 189 | editor.webCodeState.savedAlternativeVersionId = altId; 190 | editor.webCodeState.functions.checkForChanges(); 191 | }); 192 | } else if (stats.constructor === BufferFile) { 193 | return fileDialog({ 194 | path: state.get('currentlyOpenedPath') || process.env.HOME || '/', 195 | role: 'save as', 196 | filename: stats.data.name 197 | }).then(function (path) { 198 | return fs.writeFile(path, editor.getValue()) 199 | .then(function () { 200 | editor.webCodeState.savedAlternativeVersionId = altId; 201 | editor.webCodeState.functions.checkForChanges(); 202 | return Stats.fromPath(path); 203 | }) 204 | .then(function (newStats) { 205 | var tabs = tabController.getTabsAsArray(); 206 | var oldTab = tabController.getTabFromKey(stats); 207 | var index = tabs.indexOf(oldTab); 208 | var newTab = tabs[index] = tabController.newTab(newStats); 209 | tabController.closeTab(stats); 210 | tabController.setOrder(tabs); 211 | return fs.readFile(newStats.data.path, 'utf8') 212 | .then(function (fileContents) { 213 | var language = getMonacoLanguageFromMimes(stats.data.mime) || getMonacoLanguageFromExtensions(stats.data.extension); 214 | newTab.editor = monaco.editor.create(newTab.contentEl, monacoSettings({ 215 | value: fileContents, 216 | language: language 217 | })); 218 | addBindings(newTab.editor, newTab); 219 | tabController.focusTab(newTab); 220 | return newTab; 221 | }); 222 | }); 223 | }); 224 | } else { 225 | throw Error('Not a FileStats or FileBuffer'); 226 | } 227 | } 228 | 229 | export { 230 | populateFileList, 231 | openFile, 232 | openPath, 233 | promptForOpen, 234 | smartOpen, 235 | destroyFileList, 236 | saveTextFileFromEditor 237 | }; 238 | -------------------------------------------------------------------------------- /static/scripts/lib/fs-proxy.js: -------------------------------------------------------------------------------- 1 | /* global Map, Set, Promise */ 2 | /* eslint no-var: 0, no-console: 0 */ 3 | /* eslint-env es6 */ 4 | 5 | import { remoteCmd } from './ws.js'; 6 | import Stats from './web-code-stats.js' 7 | 8 | function fsProxy() { 9 | var args = Array.from(arguments); 10 | var cmd = args.shift(); 11 | 12 | function execute() { 13 | var args = Array.from(arguments); 14 | return remoteCmd('FS_PROXY', { 15 | cmd: cmd, 16 | arguments: args 17 | }) 18 | .then(function (data) { 19 | if (typeof data !== 'object') return data; 20 | if (data.__webStatDoc) return Stats.fromDoc(data); 21 | return data; 22 | }); 23 | } 24 | 25 | if (args.length === 0) return execute; 26 | return execute.apply(null, args); 27 | } 28 | 29 | var fs = {}; 30 | 31 | [ 32 | 'stat', 33 | 'readFile', 34 | 'writeFile', 35 | 'readdir', 36 | 'mkdir', 37 | 'rename', 38 | 'unlink', 39 | 'rmdir' 40 | ].forEach(function (cmd) { 41 | fs[cmd] = fsProxy(cmd); 42 | }); 43 | 44 | export default fs; 45 | -------------------------------------------------------------------------------- /static/scripts/lib/monaco.js: -------------------------------------------------------------------------------- 1 | /* global monaco, Map, Set, Promise */ 2 | /* eslint no-var: 0, no-console: 0 */ 3 | /* eslint-env es6 */ 4 | 5 | import { closeOpenTab } from './tab-controller.js'; 6 | import { promptForOpen, saveTextFileFromEditor } from './files.js'; 7 | import BufferFile from './buffer-file.js' 8 | import debounce from 'lodash.debounce'; 9 | 10 | var settings = { 11 | theme: 'web-code', 12 | fontSize: 14, 13 | fontLigatures: true, 14 | fontFamily: '"Operator Mono", "Fira Code"' 15 | } 16 | var settingsKeys = Object.keys(settings); 17 | 18 | function monacoSettings(inObj) { 19 | inObj = inObj || {}; 20 | settingsKeys.forEach(function (key) { 21 | if (inObj[key] === undefined) { 22 | inObj[key] = settings[key]; 23 | } 24 | }) 25 | return inObj; 26 | } 27 | 28 | function strHash(s) { 29 | return s.split("").reduce(function(a, b) { 30 | a = ((a << 5) - a) + b.charCodeAt(0); 31 | return a & a 32 | }, 0); 33 | } 34 | 35 | require.config({ paths: { 'vs': 'vs' } }); 36 | 37 | var monacoPromise = new Promise(function (resolve) { 38 | require(['vs/editor/editor.main'], resolve); 39 | }) 40 | .then(function () { 41 | monaco.editor.defineTheme('web-code', { 42 | base: 'vs-dark', 43 | inherit: true, 44 | rules: [ 45 | { token: 'comment', foreground: 'ffa500', fontStyle: 'italic' }, 46 | { token: 'punctuation.definition.comment', fontStyle: 'italic' }, 47 | { token: 'constant.language.this.js', fontStyle: 'italic' }, 48 | { token: 'variable.language', fontStyle: 'italic' }, 49 | { token: 'entity.other.attribute-name', fontStyle: 'italic' }, 50 | { token: 'tag.decorator.js', fontStyle: 'italic' }, 51 | { token: 'entity.name.tag.js,', fontStyle: 'italic' }, 52 | { token: 'tag.decorator.js', fontStyle: 'italic' }, 53 | { token: 'punctuation.definition.tag.js', fontStyle: 'italic' }, 54 | { token: 'source.js', fontStyle: 'italic' }, 55 | { token: 'constant.other.object.key.js', fontStyle: 'italic' }, 56 | { token: 'string.unquoted.label.js', fontStyle: 'italic' }, 57 | ] 58 | }); 59 | }); 60 | 61 | function getMonacoLanguageFromMimes(mime) { 62 | return (monaco.languages.getLanguages().filter(function (languageObj) { 63 | return languageObj.mimetypes && languageObj.mimetypes.includes(mime); 64 | })[0] || {})['id']; 65 | } 66 | 67 | function getMonacoLanguageFromExtensions(extension) { 68 | return (monaco.languages.getLanguages().filter(function (languageObj) { 69 | return languageObj.extensions && languageObj.extensions.includes(extension); 70 | })[0] || {})['id']; 71 | } 72 | 73 | function selectNextEl() { 74 | document.querySelector('a, button, [tabindex]').focus(); 75 | } 76 | 77 | function selectPreviousEl() { 78 | document.querySelectorAll('a, button, [tabindex]').focus(); 79 | } 80 | 81 | function nextTab() { 82 | console.log('STUB: FOCUS NEXT TAB'); 83 | } 84 | 85 | function previousTab() { 86 | console.log('STUB: FOCUS PREVIOUS TAB'); 87 | } 88 | 89 | function addBindings(editor, tab) { 90 | editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_O, promptForOpen); 91 | editor.addCommand(monaco.KeyCode.KEY_W | monaco.KeyMod.CtrlCmd, closeOpenTab); 92 | editor.addCommand(monaco.KeyCode.F6, selectNextEl); 93 | editor.addCommand(monaco.KeyCode.F6 | monaco.KeyMod.Shift, selectPreviousEl); 94 | editor.addCommand(monaco.KeyCode.Tab | monaco.KeyMod.CtrlCmd, nextTab); 95 | editor.addCommand(monaco.KeyCode.Tab | monaco.KeyMod.Shift | monaco.KeyMod.CtrlCmd, previousTab); 96 | editor.addCommand(monaco.KeyCode.KEY_P | monaco.KeyMod.Shift | monaco.KeyMod.CtrlCmd, function openCommandPalette() { 97 | editor.trigger('anyString', 'editor.action.quickCommand'); 98 | }); 99 | editor.addCommand(monaco.KeyCode.Tab, function() { 100 | selectNextEl(); 101 | }, 'hasJustTabbedIn') 102 | 103 | editor.webCodeState = {}; 104 | editor.webCodeState.textHash = strHash(editor.getValue()) 105 | editor.webCodeState.tab = tab; 106 | editor.webCodeState.hasJustTabbedIn = editor.createContextKey('hasJustTabbedIn', false); 107 | 108 | editor.webCodeState.functions = { 109 | checkForChanges: function checkForChanges() { 110 | editor.webCodeState.hasJustTabbedIn.set(false); 111 | let hasChanges = editor.webCodeState.textHash !== strHash(editor.getValue()) 112 | editor.webCodeState.hasChanges = hasChanges; 113 | tab.el.classList.toggle('has-changes', editor.webCodeState.hasChanges); 114 | } 115 | } 116 | 117 | var writeToDB = debounce(function writeToDB() { 118 | if (tab.stats.constructor === BufferFile) { 119 | tab.stats.update(editor.getValue()).then(function () { 120 | editor.webCodeState.functions.checkForChanges(); 121 | }); 122 | } 123 | }, 500); 124 | 125 | editor.onDidChangeModelContent(function () { 126 | writeToDB(); 127 | editor.webCodeState.functions.checkForChanges(); 128 | }); 129 | 130 | editor.onDidFocusEditorText(function () { 131 | editor.webCodeState.hasJustTabbedIn.set(true); 132 | }); 133 | editor.onMouseDown(function () { 134 | editor.webCodeState.hasJustTabbedIn.set(false); 135 | }); 136 | 137 | editor.addAction({ 138 | id: 'web-code-save-tab', 139 | label: 'Save File', 140 | keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S], 141 | keybindingContext: null, 142 | run: function () { 143 | editor.webCodeState.textHash = strHash(editor.getValue()) 144 | saveTextFileFromEditor(tab.stats, editor); 145 | } 146 | }); 147 | } 148 | 149 | export { monacoPromise, getMonacoLanguageFromExtensions, getMonacoLanguageFromMimes, addBindings, monacoSettings }; 150 | -------------------------------------------------------------------------------- /static/scripts/lib/newFile.js: -------------------------------------------------------------------------------- 1 | /* global Map, Set, Promise, monaco */ 2 | /* eslint no-var: 0, no-console: 0 */ 3 | /* eslint-env es6 */ 4 | 5 | import { tabController } from './tab-controller.js'; 6 | import { monacoPromise, monacoSettings, addBindings } from './monaco.js'; 7 | import BufferFile from './buffer-file.js'; 8 | 9 | // Until they are saved new files are kept in a buffer 10 | // Saved on changes to the db when saved to disk they are removed from the DB 11 | function newFile() { 12 | var tab; 13 | 14 | var bf = new BufferFile ({ 15 | name: 'New File', 16 | icon: 'buffer', 17 | id: Date.now() + '__' + 'New File' 18 | }); 19 | 20 | bf.valuePromise 21 | .then(function () { 22 | tab = tabController.newTab(bf); 23 | return monacoPromise; 24 | }) 25 | .then(function () { 26 | tab.editor = monaco.editor.create(tab.contentEl, monacoSettings()); 27 | addBindings(tab.editor, tab); 28 | tabController.focusTab(tab); 29 | }) 30 | .catch(function (e) { 31 | console.log(e.message); 32 | }); 33 | } 34 | 35 | 36 | // Add a special tab for taking notes. 37 | // The idea of the scratch is that it is always present 38 | // 39 | // Different directories haave their own scratch 40 | // 41 | // Saving it makes a new file but launches a new empty scratch 42 | function setUpScratch() { 43 | var tab = tabController.newTab(new BufferFile ({ 44 | name: 'Scratchpad', 45 | icon: 'buffer' 46 | })); 47 | 48 | // Puts new tab at the start rest get moved after it 49 | tabController.setOrder([ 50 | tab 51 | ]); 52 | 53 | return monacoPromise 54 | .then(function () { 55 | tab.editor = monaco.editor.create(tab.contentEl, monacoSettings()); 56 | addBindings(tab.editor, tab); 57 | }) 58 | .catch(function (e) { 59 | console.log(e.message); 60 | }); 61 | } 62 | 63 | export { 64 | newFile, 65 | setUpScratch 66 | } -------------------------------------------------------------------------------- /static/scripts/lib/render-file-list.js: -------------------------------------------------------------------------------- 1 | /* global Map, Set, Promise */ 2 | /* eslint no-var: 0, no-console: 0 */ 3 | /* eslint-env es6 */ 4 | 5 | import Stats from './web-code-stats.js'; 6 | import BufferFile from './buffer-file.js'; 7 | 8 | function renderFileList(el, array, options) { 9 | 10 | options = options || {}; 11 | var useOptions = { 12 | hideDotFiles: (options.hideDotFiles !== undefined ? options.hideDotFiles : true), 13 | nested: (options.nested !== undefined ? options.nested : true), 14 | nestingLimit: (options.nestingLimit || 5) - 1, 15 | sort: options.sort === false ? false : true 16 | } 17 | if (options.nestingLimit === 0) return; 18 | 19 | var sortedData = !useOptions.sort ? array : Array.from(array) 20 | .filter(function (stats) { 21 | 22 | // Whether to hide dotfiles 23 | if (stats.data.name !== '..' && useOptions.hideDotFiles !== false) { 24 | return stats.data.name[0] !== '.'; 25 | } 26 | return true; 27 | }) 28 | .sort(function (a, b) { 29 | if (a.name === '..') { 30 | return -1; 31 | } 32 | if (b.name === '..') { 33 | return 1; 34 | } 35 | if ( 36 | (a.isDirectory() === b.isDirectory()) && 37 | (a.isFile() === b.isFile()) 38 | ) { 39 | return ([a.data.name, b.data.name].sort(function (a, b) { 40 | return a.toLowerCase().localeCompare(b.toLowerCase()); 41 | })[0] === a.data.name ? -1 : 1); 42 | } else { 43 | if (a.isDirectory()) return -1; 44 | return 1; 45 | } 46 | }); 47 | 48 | sortedData.map(function (stats) { 49 | 50 | var li = document.createElement('li'); 51 | li.classList.add('has-icon'); 52 | li.tabIndex = 0; 53 | li.tabKey = stats; 54 | 55 | if (stats.constructor === Stats) { 56 | li.dataset.mime = stats.data.mime; 57 | li.dataset.name = stats.data.name; 58 | li.dataset.size = stats.data.size; 59 | li.textContent = stats.data.name; 60 | li.stats = stats; 61 | 62 | if (stats.isDirectory() && useOptions.nested !== false) { 63 | var newFileList = document.createElement('ul'); 64 | newFileList.classList.add('filelist'); 65 | li.appendChild(newFileList); 66 | if (stats.expanded && stats.children) { 67 | stats.renderFileList(newFileList, useOptions); 68 | } 69 | } 70 | } else if (stats.constructor === BufferFile) { 71 | li.dataset.name = stats.data.name; 72 | li.textContent = stats.data.name; 73 | if (stats.data.icon) { 74 | li.classList.add('has-icon'); 75 | li.dataset.icon = stats.icon; 76 | } 77 | if (stats.data.mime) { 78 | li.dataset.mime = stats.mime; 79 | } 80 | } 81 | 82 | el.appendChild(li); 83 | }); 84 | 85 | 86 | } 87 | 88 | export default renderFileList; -------------------------------------------------------------------------------- /static/scripts/lib/side-bar.js: -------------------------------------------------------------------------------- 1 | /* global Map, Set, Promise, contextmenu */ 2 | /* eslint no-var: 0, no-console: 0 */ 3 | /* eslint-env es6 */ 4 | 5 | import { populateFileList, destroyFileList, openFile } from './files.js'; 6 | import { join } from 'path'; 7 | import fs from './fs-proxy.js'; 8 | import Stats from './web-code-stats.js'; 9 | import { displayError } from './errors.js'; 10 | 11 | function setUpSideBar() { 12 | 13 | function expandDir(el, stats) { 14 | var filelistEl = el.querySelector('.filelist'); 15 | if (el.stats.expanded === true) { 16 | el.stats.expanded = false; 17 | destroyFileList(filelistEl); 18 | } else { 19 | el.stats.expanded = true; 20 | populateFileList(filelistEl, stats.data.path, { 21 | hideDotFiles: true 22 | }); 23 | } 24 | } 25 | 26 | function refreshSideBar() { 27 | directoryEl.stats.updateChildren(); 28 | } 29 | 30 | var directoryEl = document.querySelector('#directory'); 31 | 32 | function onclick(e) { 33 | if (e.target.tagName === 'LI') { 34 | if (e.target.stats.isFile()) openFile(e.target.stats); 35 | if (e.target.stats.isDirectory()) expandDir(e.target, e.target.stats); 36 | } 37 | } 38 | 39 | function onkeydown(e) { 40 | if (event.keyCode === 13) onclick(e); 41 | } 42 | 43 | var menu = contextmenu([ 44 | { 45 | label: 'New File', 46 | onclick: function () { 47 | lastContextEl = lastContextEl || directoryEl; 48 | if (lastContextEl.stats) { 49 | var newFile = prompt('New file name:', 'untitled.txt'); 50 | if (newFile) { 51 | var newPath = join(lastContextEl.stats.isDirectory() ? lastContextEl.stats.data.path : lastContextEl.stats.data.dirName, newFile); 52 | fs.writeFile(newPath, '', { 53 | flag: 'wx' 54 | }) 55 | .then(function () { 56 | return Stats.fromPath(newPath) 57 | }) 58 | .then(function (stats) { 59 | openFile(stats); 60 | }) 61 | .catch(function (e) { 62 | displayError('FS Error', e.message, 3000); 63 | }) 64 | .then(refreshSideBar); 65 | } 66 | } 67 | } 68 | }, 69 | { 70 | label: 'New Folder', 71 | onclick: function () { 72 | lastContextEl = lastContextEl || directoryEl; 73 | if (lastContextEl.stats) { 74 | var newFolder = prompt('New folder name:', 'New Folder'); 75 | if (newFolder) { 76 | fs.mkdir(join(lastContextEl.stats.isDirectory() ? lastContextEl.stats.data.path : lastContextEl.stats.data.dirName, newFolder)).then(function () { 77 | console.log('success'); 78 | }) 79 | .catch(function (e) { 80 | displayError('FS Error', e.message, 3000); 81 | }) 82 | .then(refreshSideBar); 83 | } 84 | } 85 | } 86 | }, 87 | { 88 | label: 'Rename', 89 | onclick: function () { 90 | lastContextEl = lastContextEl || directoryEl; 91 | if (lastContextEl.stats) { 92 | var newName = prompt('Rename file:', lastContextEl.stats.data.name); 93 | if (newName) { 94 | fs.rename(lastContextEl.stats.data.path, join(lastContextEl.stats.data.dirName, newName)).then(function () { 95 | console.log('success'); 96 | }) 97 | .catch(function (e) { 98 | displayError('FS Error', e.message, 3000); 99 | }) 100 | .then(refreshSideBar); 101 | } 102 | } 103 | } 104 | }, 105 | { 106 | label: 'Delete File', 107 | onclick: function () { 108 | lastContextEl = lastContextEl || directoryEl; 109 | if (lastContextEl.stats) { 110 | var path = join(lastContextEl.stats.data.path); 111 | var confirmDel = confirm('Are you sure you want to delete this file?\n' + path); 112 | if (confirmDel) { 113 | if (lastContextEl.stats.isFile()) { 114 | fs.unlink(path).then(function () { 115 | console.log('success'); 116 | }) 117 | .catch(function (e) { 118 | displayError('FS Error', e.message, 3000); 119 | }) 120 | .then(refreshSideBar); 121 | } 122 | if (lastContextEl.stats.isDirectory()) { 123 | fs.rmdir(path).then(function () { 124 | console.log('success'); 125 | }) 126 | .catch(function (e) { 127 | displayError('FS Error', e.message, 3000); 128 | }) 129 | .then(refreshSideBar); 130 | } 131 | } 132 | } 133 | } 134 | }, 135 | ]); 136 | 137 | var lastContextEl; 138 | 139 | function updateContextMenuEl(el) { 140 | el = el || {}; 141 | if (el.stats) { 142 | lastContextEl = el; 143 | menuTitle.textContent = el.stats.data.name; 144 | } else { 145 | lastContextEl = null; 146 | menuTitle.textContent = ''; 147 | } 148 | } 149 | 150 | directoryEl.addEventListener('contextmenu', function (e) { 151 | updateContextMenuEl(e.target); 152 | setTimeout(function () { menu.focus(); }, 0); 153 | }); 154 | 155 | menu.tabIndex = 0; 156 | 157 | menu.addEventListener('keydown', function (e) { 158 | var children = Array.from(menu.children); 159 | var cur = menu.querySelector(':focus'); 160 | var index = cur ? children.indexOf(cur) : -1; 161 | switch (e.keyCode) { 162 | case 38: // up 163 | index--; 164 | if (index < 0) index = children.length - 1; 165 | break; 166 | case 40: // down 167 | index++; 168 | if (index >= children.length) index = 0; 169 | break; 170 | case 13: 171 | if (cur) cur.click(); 172 | break; 173 | } 174 | if (children[index]) children[index].focus(); 175 | }) 176 | 177 | Array.from(menu.querySelectorAll('menuitem')).forEach(function (el) { 178 | el.tabIndex=0; 179 | }); 180 | 181 | contextmenu.attach(directoryEl, menu); 182 | 183 | var menuTitle = document.createElement('h2'); 184 | menuTitle.textContent = 'Directory'; 185 | menu.insertBefore(menuTitle, menu.firstChild); 186 | 187 | window.addEventListener('load', function () { 188 | var overlay = menu.parentNode; 189 | overlay.addEventListener('contextmenu', function (e) { 190 | var display = overlay.style.display; 191 | overlay.style.display = 'none'; 192 | var node = document.elementFromPoint(e.clientX, e.clientY); 193 | if (node === directoryEl || directoryEl.contains(node)) { 194 | updateContextMenuEl(node); 195 | } 196 | overlay.style.display = display; 197 | setTimeout(function () { menu.focus(); }, 0); 198 | }); 199 | }); 200 | 201 | directoryEl.addEventListener('click', onclick); 202 | directoryEl.addEventListener('keydown', onkeydown); 203 | 204 | }; 205 | 206 | export { setUpSideBar }; -------------------------------------------------------------------------------- /static/scripts/lib/state.js: -------------------------------------------------------------------------------- 1 | /* eslint no-var: 0 */ 2 | import { remoteCmd } from './ws.js'; 3 | var data = {}; 4 | 5 | export default { 6 | set: function set(key, datum) { 7 | data[key] = datum; 8 | }, 9 | 10 | sync: function () { 11 | 12 | // Tell the server the root path for this window has changed 13 | return remoteCmd('SYNC_STATE', data); 14 | }, 15 | 16 | get: function get(key) { 17 | return data[key]; 18 | } 19 | }; -------------------------------------------------------------------------------- /static/scripts/lib/tab-controller.js: -------------------------------------------------------------------------------- 1 | /* global Map, Set, Promise */ 2 | /* eslint no-var: 0, no-console: 0 */ 3 | /* eslint-env es6 */ 4 | 5 | import state from './state.js'; 6 | import { updateDBDoc } from './db.js'; 7 | import Stats from './web-code-stats.js' 8 | import renderFileList from './render-file-list.js'; 9 | import { saveTextFileFromEditor } from './files.js'; 10 | import BufferFile from './buffer-file.js'; 11 | 12 | function saveOpenTab() { 13 | var tab = tabController.getOpenTab(); 14 | var stats; 15 | if (tab && tab.editor) { 16 | stats = tab.stats; 17 | } else { 18 | return; 19 | } 20 | saveTextFileFromEditor(stats, tab.editor); 21 | } 22 | 23 | function closeOpenTab() { 24 | var tab = tabController.getOpenTab(); 25 | if (tab) tabController.closeTab(tab); 26 | } 27 | 28 | var tabController = (function setUpTabs() { 29 | var currentlyOpenFilesEl = document.querySelector('#currently-open-files'); 30 | var containerEl = document.getElementById('container'); 31 | var tabsEl = document.querySelector('#tabs'); 32 | 33 | function updateOpenFileEl() { 34 | currentlyOpenFilesEl.innerHTML = ''; 35 | renderFileList(currentlyOpenFilesEl, Array.from(tabController.currentlyOpenFilesMap.keys()), { 36 | sort: false 37 | }); 38 | } 39 | 40 | function Tab(stats) { 41 | 42 | var addCloseButton = false; 43 | 44 | // It is a reference to a file 45 | if (stats.constructor === Stats) { 46 | this.stats = stats; 47 | this.el = document.createElement('a'); 48 | this.el.classList.add('tab'); 49 | this.el.classList.add('has-icon'); 50 | this.el.dataset.mime = stats.data.mime; 51 | this.el.dataset.name = stats.data.name; 52 | this.el.textContent = stats.data.name; 53 | this.el.tabIndex = 0; 54 | addCloseButton = true; 55 | } else if (stats.constructor === BufferFile) { 56 | this.stats = stats; 57 | this.el = document.createElement('a'); 58 | this.el.classList.add('tab'); 59 | this.el.dataset.name = stats.data.name; 60 | this.el.textContent = stats.data.name; 61 | if (stats.data.icon) { 62 | this.el.classList.add('has-icon'); 63 | this.el.dataset.icon = stats.data.icon; 64 | } 65 | if (stats.data.mime) { 66 | this.el.dataset.mime = stats.data.mime; 67 | } 68 | if (stats.data.hasTabCloseButton !== false) { 69 | addCloseButton = true; 70 | } 71 | this.el.tabIndex = 0; 72 | } 73 | 74 | if (addCloseButton) { 75 | 76 | this.closeEl = document.createElement('button'); 77 | this.closeEl.classList.add('tab_close'); 78 | this.closeEl.setAttribute('aria-label', 'Close Tab ' + stats.data.name); 79 | this.el.appendChild(this.closeEl); 80 | this.closeEl.tabIndex = 0; 81 | 82 | var self = this; 83 | this.closeEl.addEventListener('click', function () { 84 | tabController.closeTab(self); 85 | }); 86 | } 87 | 88 | tabsEl.appendChild(this.el); 89 | 90 | this.el.webCodeTab = this; 91 | 92 | this.contentEl = document.createElement('div'); 93 | this.contentEl.classList.add('tab-content'); 94 | containerEl.appendChild(this.contentEl); 95 | 96 | } 97 | 98 | Tab.prototype.destroy = function () { 99 | this.el.parentNode.removeChild(this.el); 100 | this.contentEl.parentNode.removeChild(this.contentEl); 101 | } 102 | 103 | function TabController() { 104 | this.currentlyOpenFilesMap = new Map(); 105 | this.focusedTab = null; 106 | } 107 | 108 | TabController.prototype.hasTab = function (stats) { 109 | return this.currentlyOpenFilesMap.has(stats); 110 | } 111 | 112 | TabController.prototype.getOpenTab = function () { 113 | return this.focusedTab; 114 | } 115 | 116 | TabController.prototype.newTab = function (stats) { 117 | var tab = new Tab(stats); 118 | this.currentlyOpenFilesMap.set(stats, tab); 119 | updateOpenFileEl(); 120 | this.storeOpenTabs(); 121 | 122 | if (!this.focusedTab) { 123 | this.focusTab(tab); 124 | } 125 | 126 | return tab; 127 | } 128 | 129 | TabController.prototype.focusTab = function (stats) { 130 | var focusedTab = stats.constructor === Tab ? stats : this.currentlyOpenFilesMap.get(stats); 131 | this.focusedTab = focusedTab; 132 | Array.from(this.currentlyOpenFilesMap.values()).forEach(function (tab) { 133 | tab.contentEl.classList.toggle('has-focus', tab === focusedTab); 134 | tab.el.classList.toggle('has-focus', tab === focusedTab); 135 | }); 136 | if (focusedTab.editor) focusedTab.editor.layout(); 137 | } 138 | 139 | TabController.prototype.getTabFromKey = function (stats) { 140 | var tab = stats.constructor === Tab ? stats : this.currentlyOpenFilesMap.get(stats); 141 | return tab; 142 | } 143 | 144 | TabController.prototype.closeTab = function (stats) { 145 | var tab = stats.constructor === Tab ? stats : this.currentlyOpenFilesMap.get(stats); 146 | var tabState = Array.from(this.currentlyOpenFilesMap.values()); 147 | var tabIndex = tabState.indexOf(tab); 148 | var nextTab = (tabIndex >= 1) ? tabState[tabIndex - 1] : tabState[tabIndex + 1] ; 149 | 150 | this.currentlyOpenFilesMap.delete(tab.stats); 151 | tab.destroy(); 152 | updateOpenFileEl(); 153 | this.storeOpenTabs(); 154 | if (this.focusedTab === tab && nextTab) { 155 | this.focusTab(nextTab); 156 | } 157 | if (this.focusedTab === tab) { 158 | this.focusedTab = null; 159 | } 160 | // if (this.currentlyOpenFilesMap.size === 0) { 161 | // newFile(); 162 | // } 163 | } 164 | 165 | TabController.prototype.closeAll = function () { 166 | var self=this; 167 | Array.from(this.currentlyOpenFilesMap.values()).forEach(function (tab) { 168 | self.closeTab(tab); 169 | }); 170 | } 171 | 172 | TabController.prototype.storeOpenTabs = function () { 173 | if (!state.get('currentlyOpenedPath')) return; 174 | updateDBDoc('OPEN_TABS_FOR_' + state.get('currentlyOpenedPath'), { 175 | open_tabs: Array.from(this.currentlyOpenFilesMap.keys()).map(function (stats) { 176 | return stats.toDoc(); 177 | }) 178 | }) 179 | .catch(function (err) { 180 | console.log(err); 181 | }); 182 | } 183 | 184 | TabController.prototype.getTabsAsArray = function () { 185 | return Array.from(tabsEl.children); 186 | } 187 | 188 | /** 189 | * All the elements in the array are moved to the start in the order they appear. 190 | */ 191 | TabController.prototype.setOrder= function(arr) { 192 | var old = new Set(this.getTabsAsArray()); 193 | tabsEl.innerHTML = ''; 194 | arr.forEach(function (el) { 195 | if (el.constructor === Tab) { 196 | el = el.el; 197 | } 198 | if (old.has(el.el)) { 199 | tabsEl.appendChild(el); 200 | old.delete(el); 201 | } 202 | }); 203 | Array.from(old).forEach(function (el) { 204 | tabsEl.appendChild(el); 205 | }); 206 | } 207 | 208 | var tabController = new TabController(); 209 | 210 | tabsEl.addEventListener('mouseup', function (e) { 211 | if (e.target.matches('.tab')) { 212 | if (e.button === 0) { 213 | tabController.focusTab(e.target.webCodeTab); 214 | } 215 | if (e.button === 1) { 216 | tabController.closeTab(e.target.webCodeTab); 217 | } 218 | } 219 | }); 220 | 221 | currentlyOpenFilesEl.addEventListener('mouseup', function (e) { 222 | if (e.target.tabKey) { 223 | if (e.button === 0) { 224 | tabController.focusTab(e.target.tabKey); 225 | } 226 | if (e.button === 1) { 227 | tabController.closeTab(e.target.tabKey); 228 | } 229 | } 230 | }); 231 | 232 | return tabController; 233 | }()); 234 | 235 | export { 236 | saveOpenTab, 237 | closeOpenTab, 238 | tabController 239 | }; -------------------------------------------------------------------------------- /static/scripts/lib/utils.js: -------------------------------------------------------------------------------- 1 | /* global Map, Set, Promise */ 2 | /* eslint no-var: 0, no-console: 0 */ 3 | /* eslint-env es6 */ 4 | 5 | function addScript (url) { 6 | var p = new Promise(function (resolve, reject) { 7 | var script = document.createElement('script'); 8 | script.setAttribute('src', url); 9 | document.head.appendChild(script); 10 | script.onload = resolve; 11 | script.onerror = reject; 12 | }); 13 | function promiseScript () { 14 | return p; 15 | }; 16 | promiseScript.promise = p; 17 | return promiseScript; 18 | } 19 | 20 | export { 21 | addScript 22 | } -------------------------------------------------------------------------------- /static/scripts/lib/web-code-stats.js: -------------------------------------------------------------------------------- 1 | /* global Map, Set, Promise, fs, isServer */ 2 | /* eslint no-var: 0, no-console: 0 */ 3 | /* eslint-env es6 */ 4 | 5 | import { resolve as pathResolve, basename, dirname, extname, join } from 'path'; 6 | import mime from 'mime'; 7 | import renderFileList from './render-file-list.js'; 8 | import { remoteCmd } from './ws.js'; 9 | 10 | // Map to prevent duplicate data objects for each file 11 | var pathToDataMap = new Map(); 12 | 13 | var fsFromFn = ['isFile', 'isDirectory', 'isBlockDevice', 'isCharacterDevice', 'isSymbolicLink', 'isFIFO', 'isSocket']; 14 | var fsStatic = [ 15 | 'dev', 16 | 'mode', 17 | 'nlink', 18 | 'uid', 19 | 'gid', 20 | 'rdev', 21 | 'blksize', 22 | 'ino', 23 | 'size', 24 | 'blocks', 25 | 'atime', 26 | 'mtime', 27 | 'ctime', 28 | 'birthtime', 29 | 'path' 30 | ]; 31 | var keys = fsStatic.concat(fsFromFn); 32 | 33 | 34 | /** 35 | * Special type of singleton which returns the same object for each path. 36 | */ 37 | export default function Stats (data) { 38 | if (pathToDataMap.has(data.path)) { 39 | var existing = pathToDataMap.get(data.path); 40 | existing.update(data); 41 | return existing; 42 | } 43 | this.fileLists = new Set(); 44 | this.data = {}; 45 | this.update(data); 46 | pathToDataMap.set(data.path, this); 47 | } 48 | 49 | Stats.prototype.update = function update(data) { 50 | 51 | var self = this; 52 | 53 | this.data.name = basename(data.path); 54 | this.data.dirName = dirname(data.path); 55 | this.data.extension = extname(data.path).toLowerCase(); 56 | this.data.mime = data.isFile ? mime.lookup(data.path) : 'directory'; 57 | 58 | keys.forEach(function (key) { 59 | this.data[key] = data[key]; 60 | }.bind(this)); 61 | 62 | if (this.isDirectory() && !this.children) { 63 | this.children = []; 64 | this.childrenPopulated = false; 65 | } 66 | 67 | // Rerender file lists 68 | if (this.fileLists.size) { 69 | Array.from(this.fileLists).forEach(function (filelistEl) { 70 | filelistEl.innerHTML = ''; 71 | self.renderFileList(filelistEl, filelistEl.filelistOptions); 72 | }); 73 | } 74 | } 75 | 76 | Stats.prototype.toDoc = function toDoc() { 77 | var out = { 78 | __webStatDoc: true 79 | }; 80 | keys.forEach(function (key) { 81 | out[key] = this.data[key]; 82 | }.bind(this)); 83 | return out; 84 | } 85 | 86 | Stats.prototype.updateChildren = function () { 87 | if(!this.isDirectory()) throw Error('Not a directory'); 88 | 89 | var self = this; 90 | return fs.readdir(self.data.path) 91 | .then(function (arr) { 92 | return Promise.all(arr.map(function (child) { 93 | return Stats.fromPath(join(self.data.path, child)); 94 | })); 95 | }) 96 | .then(function (statsArray) { 97 | self.children.splice(0); 98 | self.children.push.apply(self.children, statsArray); 99 | 100 | // Let server know 101 | if (!isServer) remoteCmd('CLIENT', { 102 | cmd: 'watchPath', 103 | arguments: [self.data.path] 104 | }); 105 | 106 | self.update(self.data); 107 | 108 | return self; 109 | }); 110 | } 111 | 112 | Stats.prototype.destroyFileList = function (el) { 113 | el.stats = undefined; 114 | this.fileLists.delete(el); 115 | el.innerHTML = ''; 116 | } 117 | 118 | Stats.prototype.renderFileList = function (el, options) { 119 | 120 | el.filelistOptions = options; 121 | 122 | el.stats = this; 123 | this.fileLists.add(el); 124 | el.dataset.mime = this.data.mime; 125 | el.dataset.name = this.data.name; 126 | el.dataset.size = this.data.size; 127 | 128 | renderFileList(el, this.children, options); 129 | } 130 | 131 | // add isFile isDirectory etc 132 | fsFromFn.forEach(function (key) { 133 | Stats.prototype[key] = new Function('return this.data["' + key + '"];'); 134 | }); 135 | 136 | Stats.fromPath = function (path) { 137 | return fs.stat(path); 138 | } 139 | 140 | Stats.fromDoc = function (data) { 141 | return new Stats(data); 142 | } 143 | 144 | Stats.fromNodeStats = function (path, nodeStat) { 145 | 146 | var out = {}; 147 | 148 | fsFromFn.forEach(key => out[key] = nodeStat[key]()); 149 | keys.forEach(key => { 150 | if (typeof nodeStat[key] !== 'function' && typeof nodeStat[key] !== 'object') { 151 | out[key] = nodeStat[key]; 152 | } 153 | }); 154 | 155 | out.path = pathResolve(path); 156 | 157 | return new Stats(out); 158 | } 159 | -------------------------------------------------------------------------------- /static/scripts/lib/ws.js: -------------------------------------------------------------------------------- 1 | /* global Map, Set, Promise, isServer */ 2 | /* eslint no-var: 0, no-console: 0 */ 3 | /* eslint-env es6 */ 4 | 5 | var promises = new Map(); 6 | import Stats from './web-code-stats.js'; 7 | import { displayError, removeError } from './errors.js'; 8 | import { dirname } from 'path'; 9 | import state from './state.js'; 10 | import { openFile, openPath } from './files.js'; 11 | 12 | function remoteCmd(cmd, data, ws) { 13 | var id = performance.now() + '_' + Math.random(); 14 | return (ws ? Promise.resolve(ws) : wsPromise).then(function (ws) { 15 | ws.send(JSON.stringify([ 16 | cmd, 17 | id, 18 | data 19 | ])); 20 | 21 | if (process.env.DEBUG) { 22 | var err = new Error(); 23 | var stack = err.stack; 24 | } 25 | 26 | return new Promise(function (resolve) { 27 | promises.set(id, resolve); 28 | }).then(function (data) { 29 | if (data.error) { 30 | if (process.env.DEBUG) { 31 | console.error(data.error, stack); 32 | } 33 | throw Error(data.error); 34 | } 35 | return data.result; 36 | }); 37 | }); 38 | } 39 | 40 | function updateEnv(name, ws) { 41 | return remoteCmd('GET_ENV', name, ws) 42 | .then(function (result) { 43 | if (result) process.env[name] = result; 44 | return result; 45 | }); 46 | } 47 | 48 | var wsPromise = getNewWS(); 49 | var errorMsg; 50 | 51 | // Connection opened 52 | function getNewWS() { 53 | return new Promise(function (resolve) { 54 | 55 | if (isServer) resolve(); 56 | 57 | var interval = -1; 58 | var isLocal = location.hostname === 'localhost' || location.hostname === '127.0.0.1'; 59 | try { 60 | var ws = new WebSocket((isLocal ? 'ws://' : 'wss://') + location.host); 61 | } catch (e) { 62 | return terminate(); 63 | } 64 | ws.binaryType = 'arraybuffer'; 65 | 66 | var isAlive = true; 67 | 68 | ws.addEventListener('message', function m(e) { 69 | if (typeof e.data === 'string') { 70 | if (e.data === '__pong__') { 71 | isAlive = true; 72 | return; 73 | } 74 | var result = JSON.parse(e.data); 75 | var cmd = result[0]; 76 | var promiseResolver = promises.get(result[1]); 77 | var data = result[2]; 78 | if (promiseResolver) { 79 | promises.delete(result[1]); 80 | return promiseResolver(data); 81 | } 82 | if (cmd === 'HANDSHAKE') { 83 | Stats.fromPath(data.path).then(function (stats) { 84 | openPath(stats); 85 | }); 86 | resolve( 87 | Promise.all([ 88 | updateEnv('HOME', ws), 89 | updateEnv('DEBUG', ws), 90 | ]) 91 | .then(function () { 92 | return ws; 93 | }) 94 | ); 95 | } 96 | if (cmd === 'FS_CHANGE') { 97 | console.log('CHANGE', data); 98 | } 99 | if (cmd === 'FS_ADD') { 100 | Stats.fromPath(dirname(data.path)).then(function (stats) { 101 | stats.updateChildren(); 102 | }); 103 | console.log('ADD', data); 104 | } 105 | if (cmd === 'FS_UNLINK') { 106 | Stats.fromPath(dirname(data.path)).then(function (stats) { 107 | stats.updateChildren(); 108 | }); 109 | console.log('UNLINK', data); 110 | } 111 | if (cmd === 'OPEN_FILE') { 112 | Stats.fromPath(data.path).then(function (stats) { 113 | openFile(stats); 114 | }); 115 | } 116 | } 117 | }); 118 | 119 | ws.addEventListener('close', terminate); 120 | 121 | ws.addEventListener('open', function firstOpen() { 122 | 123 | if (errorMsg) { 124 | removeError(errorMsg); 125 | errorMsg = null; 126 | } 127 | 128 | interval = setInterval(function ping() { 129 | if (isAlive === false) { 130 | terminate(); 131 | } 132 | isAlive = false; 133 | ws.send('__ping__'); 134 | }, 3000); 135 | 136 | ws.removeEventListener('open', firstOpen); 137 | }); 138 | 139 | function terminate() { 140 | clearInterval(interval); 141 | wsPromise = new Promise(function (resolve) { 142 | if (!errorMsg) errorMsg = displayError('Connection', 'Lost server connection.'); 143 | setTimeout(function () { 144 | console.log('Trying to get new connection'); 145 | getNewWS().then(function (newWs) { 146 | resolve(newWs); 147 | }); 148 | }, 1000); 149 | }).then(function (newWs) { 150 | 151 | // don't return sync otherwise recusrion 152 | state.sync(); 153 | return newWs; 154 | }); 155 | return wsPromise; 156 | } 157 | }); 158 | } 159 | 160 | export { 161 | wsPromise, 162 | remoteCmd 163 | }; 164 | -------------------------------------------------------------------------------- /static/scripts/main.js: -------------------------------------------------------------------------------- 1 | /* global Map, Set, Promise */ 2 | /* eslint no-var: 0, no-console: 0 */ 3 | /* eslint-env es6 */ 4 | 5 | import { db } from './lib/db.js'; 6 | import { wsPromise } from './lib/ws.js'; 7 | import { openPath, promptForOpen, smartOpen } from './lib/files.js'; 8 | import { saveOpenTab, tabController } from './lib/tab-controller.js'; 9 | import { setUpSideBar } from './lib/side-bar.js'; 10 | import { addScript } from './lib/utils.js'; 11 | import { newFile } from './lib/newFile.js'; 12 | import Stats from './lib/web-code-stats.js'; 13 | import fs from './lib/fs-proxy'; 14 | window.fs = fs; 15 | 16 | wsPromise.then(function init() { 17 | 18 | console.log('Connected to the server...'); 19 | 20 | if (process.env.DEBUG) { 21 | addScript('/axe/axe.min.js').promise.then(function () { 22 | window.axe.run(function (err, results) { 23 | if (err) throw err; 24 | console.log('a11y violations:', results.violations.length, results.violations); 25 | }); 26 | }); 27 | } 28 | 29 | // load old state 30 | return db.get('INIT_STATE') 31 | .then(function (doc) { 32 | if (doc.previous_path) { 33 | return Stats.fromPath(doc.previous_path.path).then(openPath); 34 | } else { 35 | return promptForOpen(); 36 | } 37 | }) 38 | .catch(function (err) { 39 | console.log(err); 40 | }); 41 | }, function (e) { 42 | console.log(e); 43 | }); 44 | 45 | (function setUpToolBar() { 46 | document.querySelector('button[data-action="open-file"]').addEventListener('click', promptForOpen); 47 | document.querySelector('button[data-action="save-file"]').addEventListener('click', saveOpenTab); 48 | document.querySelector('button[data-action="new-file"]').addEventListener('click', newFile); 49 | }()); 50 | 51 | window.addEventListener('resize', function () { 52 | var tab = tabController.getOpenTab(); 53 | if (tab && tab.editor) tab.editor.layout(); 54 | }); 55 | 56 | setUpSideBar(); -------------------------------------------------------------------------------- /static/scripts/sw-v1.js: -------------------------------------------------------------------------------- 1 | /* global toolbox */ 2 | 3 | toolbox.router.default = toolbox.fastest; 4 | -------------------------------------------------------------------------------- /static/styles/icons.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: FontAwesome; 3 | font-weight: normal; 4 | font-style: normal; 5 | src: url("/icons/fontawesome.woff2"); 6 | } 7 | 8 | @font-face { 9 | font-family: Mfizz; 10 | src: url("/icons/mfixx.woff2"); 11 | font-weight: normal; 12 | font-style: normal; 13 | } 14 | 15 | @font-face { 16 | font-family: Devicons; 17 | src: url("/icons/devopicons.woff2"); 18 | font-weight: normal; 19 | font-style: normal; 20 | } 21 | 22 | @font-face { 23 | font-family: file-icons; 24 | src: url("/icons/file-icons.woff2"); 25 | font-weight: normal; 26 | font-style: normal; 27 | } 28 | 29 | button.tab_close:before { 30 | font-family: "FontAwesome"; 31 | content: "\f00d"; 32 | } 33 | 34 | button[data-action="open-file"]:before { 35 | font-family: "FontAwesome"; 36 | content: "\f07b"; 37 | } 38 | 39 | button[data-action="save-file"]:before { 40 | font-family: "FontAwesome"; 41 | content: "\f0c7"; 42 | } 43 | 44 | button[data-action="new-file"]:before { 45 | font-family: "FontAwesome"; 46 | content: "\f15c"; 47 | } 48 | 49 | button[data-action="up-dir"]:before { 50 | font-family: "FontAwesome"; 51 | content: "\f148"; 52 | font-size: 2em; 53 | } 54 | 55 | 56 | .has-icon:before { 57 | font-family: "FontAwesome"; 58 | content: "\f15b"; 59 | } 60 | 61 | .has-icon[data-icon="buffer"]:before { 62 | font-family: "Devicons"; 63 | content: "\E606"; 64 | } 65 | 66 | .has-icon[data-mime="directory"]:before { 67 | font-family: "FontAwesome"; 68 | content: "\f07b"; 69 | } 70 | 71 | .has-icon[data-mime="application/pdf"]:before { 72 | font-family: "FontAwesome"; 73 | content: "\f1c1"; 74 | } 75 | 76 | .has-icon[data-mime^="image/"]:before { 77 | font-family: "FontAwesome"; 78 | content: "\f1c5"; 79 | } 80 | 81 | .has-icon[data-mime="application/javascript"]:before { 82 | font-family: MFizz; 83 | content: "\f129"; 84 | } 85 | 86 | .has-icon[data-name="package.json"]:before { 87 | font-family: "file-icons"; 88 | content: "\E91C"; 89 | } 90 | 91 | .has-icon[data-mime="text/x-markdown"]:before { 92 | font-family: "Devicons"; 93 | content: "\E63E"; 94 | } 95 | 96 | .has-icon[data-mime="text/html"]:before { 97 | font-family: "FontAwesome"; 98 | content: "\f13b"; 99 | } 100 | 101 | .has-icon[data-mime="text/css"]:before { 102 | font-family: "FontAwesome"; 103 | content: "\f13c"; 104 | } -------------------------------------------------------------------------------- /static/styles/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body, html { 6 | margin: 0; 7 | padding: 0; 8 | height: 100%; 9 | overflow: hidden; 10 | font-family: "Samsung One", "Helvetica Neue", "Calibri Light", Roboto, Helvetica, sans-serif; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | letter-spacing: 0.02em; 15 | font-size: 16px; 16 | color: #333; 17 | --bg-head-color: #333; 18 | --file-list-bg: #555; 19 | --file-list-fg: white; 20 | background-color: var(--bg-head-color); 21 | } 22 | 23 | button { 24 | font-family: "Samsung One", "Helvetica Neue", "Calibri Light", Roboto, Helvetica, sans-serif; 25 | } 26 | 27 | body { 28 | display: flex; 29 | flex-direction: column; 30 | } 31 | 32 | main { 33 | display: flex; 34 | flex-direction: row; 35 | flex-grow: 1; 36 | min-height: 0; 37 | } 38 | 39 | #container { 40 | flex-grow: 1; 41 | display: flex; 42 | background: var(--bg-head-color); 43 | position: relative; 44 | } 45 | 46 | #container:empty { 47 | align-content: center; 48 | align-items: center; 49 | } 50 | 51 | @keyframes fade-in { 52 | 0% { 53 | opacity: 0; 54 | } 55 | 100% { 56 | opacity: 1; 57 | } 58 | } 59 | 60 | #container:empty:before { 61 | content: 'Welcome to Web Code.'; 62 | font-size: calc(15vw - 20px); 63 | color: rgba(255, 255, 255, 0.4); 64 | text-align: center; 65 | opacity: 0; 66 | animation: fade-in 2s ease; 67 | animation-fill-mode: forwards; 68 | animation-delay: 500ms; 69 | } 70 | 71 | #directory { 72 | flex-grow: 2; 73 | border-width: 0; 74 | margin-top: 0.5em; 75 | background: var(--file-list-bg); 76 | color: var(--file-list-fg); 77 | } 78 | 79 | #currently-open-files { 80 | margin-top: 0.5em; 81 | border-width: 0; 82 | background: var(--file-list-bg); 83 | color: var(--file-list-fg); 84 | border-top-right-radius: 1em; 85 | min-height: 5em; 86 | } 87 | 88 | .tab-content { 89 | flex-grow: 1; 90 | display: none; 91 | background-color: var(--bg-head-color); 92 | z-index: 1; 93 | } 94 | 95 | .tab-content.image-container { 96 | overflow: auto; 97 | max-width: 100%; 98 | min-width: 0; 99 | width: 100px; 100 | } 101 | 102 | .tab-content.has-focus { 103 | display: block; 104 | } 105 | 106 | #editor { 107 | flex-grow: 1; 108 | display: flex; 109 | flex-direction: column; 110 | } 111 | 112 | #sidebar { 113 | width: 200px; 114 | max-width: 50%; 115 | display: flex; 116 | flex-direction: column; 117 | } 118 | 119 | #errors { 120 | flex-grow: 0; 121 | position: relative; 122 | padding: 0; 123 | margin: 0; 124 | } 125 | 126 | .error-type { 127 | float: left; 128 | background: #bf3b66; 129 | margin: -0.5em 0 -0.5em -0.5em; 130 | display: inline-block; 131 | padding: 0.5em; 132 | border-right: 1px solid #721c2d; 133 | } 134 | 135 | #errors > li { 136 | background: palevioletred; 137 | color: black; 138 | list-style: none; 139 | text-align: center; 140 | display: block; 141 | padding: 0.5em; 142 | } 143 | 144 | #file-dialog-widget { 145 | min-width: 300px; 146 | width: 66%; 147 | max-width: 800px; 148 | height: 500px; 149 | max-height: calc(100% - 1em); 150 | background: white; 151 | box-shadow: 0 0 3em -0.5em black; 152 | position: absolute; 153 | top: 1em; 154 | transition: transform 0.5s ease; 155 | display: flex; 156 | padding: 0.5em; 157 | z-index: 2; 158 | flex-direction: column; 159 | left: calc(50vw - 400px); 160 | } 161 | 162 | 163 | #file-dialog-widget .filelist { 164 | min-width: 40%; 165 | } 166 | 167 | @media screen and (max-width: 1200px) { 168 | #file-dialog-widget { 169 | left: 16vw; 170 | } 171 | } 172 | 173 | #file-dialog-widget.closed { 174 | transform: translateY(-100%) translateY(-3em); 175 | visibility: hidden; 176 | } 177 | 178 | #file-dialog-widget input[type="text"] { 179 | flex-grow: 1; 180 | font-size: 1em; 181 | padding-left: 0.3em; 182 | } 183 | 184 | #file-dialog-widget .buttons { 185 | text-align: right; 186 | } 187 | 188 | #file-dialog-widget .buttons button { 189 | font-size: 1.3em; 190 | } 191 | 192 | #file-dialog-widget .buttons #file-dialog-submit { 193 | filter: brightness(0.8) sepia(1) hue-rotate(50deg); 194 | } 195 | 196 | #file-dialog-widget #save-file-name { 197 | display: inline; 198 | } 199 | 200 | #file-dialog-widget[data-role="save"] #save-file-name { 201 | display: inline; 202 | } 203 | 204 | .file-dialog-widget_tools { 205 | display: flex; 206 | margin-bottom: 0.25em; 207 | flex-shrink: 0; 208 | } 209 | 210 | #file-dialog-widget .file-save-widget_tools { 211 | display: none; 212 | margin-bottom: 0.25em; 213 | flex-shrink: 0; 214 | } 215 | 216 | #file-dialog-widget[data-role="save-as"] .file-save-widget_tools { 217 | display: flex; 218 | } 219 | 220 | 221 | #tools { 222 | display: flex; 223 | padding: 0.5em; 224 | background-color: var(--bg-head-color); 225 | color: white; 226 | align-items: center; 227 | flex-shrink: 0; 228 | } 229 | 230 | #tools button { 231 | font-size: 1.5em; 232 | width: 1.3em; 233 | height: 1.3em; 234 | padding: 0; 235 | text-align: center; 236 | vertical-align: middle; 237 | cursor: pointer; 238 | color: black; 239 | } 240 | 241 | #tools button:not(:hover):not(:focus) { 242 | -webkit-appearance: none; 243 | border: 0; 244 | background: none; 245 | color: #ddd; 246 | } 247 | 248 | 249 | #file-dialog-widget .file-selectors { 250 | flex-grow: 1; 251 | display: flex; 252 | flex-direction: row; 253 | margin-bottom: 0.5em; 254 | min-height: 0; 255 | } 256 | 257 | #file-dialog-widget .file-selectors .filelist:first-child { 258 | margin-right: 0.5em; 259 | } 260 | 261 | .filelist { 262 | margin: 0; 263 | border: 1px solid grey; 264 | flex-grow: 1; 265 | overflow: auto; 266 | list-style: none; 267 | padding: 0; 268 | } 269 | 270 | .filelist:before { 271 | content: attr(data-name); 272 | padding-left: 0.25em; 273 | background-color: var(--bg-head-color); 274 | color: white; 275 | display: block; 276 | border-bottom: 3px solid grey; 277 | } 278 | 279 | .filelist .filelist:before { 280 | content: none; 281 | } 282 | 283 | .filelist .filelist { 284 | padding-left: 0.5em; 285 | border: none; 286 | width: 100%; 287 | } 288 | 289 | .filelist li { 290 | position: relative; 291 | white-space: nowrap; 292 | cursor: pointer; 293 | } 294 | 295 | .has-icon:before { 296 | content: ''; 297 | display: inline-block; 298 | width: 1.5em; 299 | padding-right: 0.5em; 300 | text-align: right; 301 | user-select: none; 302 | } 303 | 304 | .filelist li.has-highlight { 305 | background-color: orange; 306 | } 307 | 308 | #tabs { 309 | display: flex; 310 | justify-content: flex-start; 311 | align-content: flex-end; 312 | background-color: var(--bg-head-color); 313 | padding-bottom: 6px; 314 | margin-bottom: -6px; 315 | flex-shrink: 0; 316 | position: relative; 317 | overflow: hidden; 318 | height: calc(2.5em + 6px); 319 | } 320 | 321 | #logo { 322 | margin: 0 0.5em 0 0; 323 | padding: 0 1em 0 0; 324 | font-weight: 200; 325 | border-bottom: 2px solid #66a8ff; 326 | } 327 | 328 | .tab { 329 | padding: 0.5em; 330 | display: inline-block; 331 | border-style: solid; 332 | border-width: 0; 333 | background-color: lightgray; 334 | border-color: lightseagreen; 335 | border-right-width: 1px; 336 | border-top-width: 3px; 337 | transform: translateY(3px); 338 | transition: transform 0.5s ease; 339 | cursor: pointer; 340 | white-space: nowrap; 341 | outline: none; 342 | } 343 | 344 | .tab.has-changes { 345 | border-color: orangered; 346 | border-top-width: 5px; 347 | } 348 | 349 | .tab.has-icon { 350 | padding: 0.5em 0.5em 0.5em 0; 351 | } 352 | 353 | .tab:hover, .tab:focus, .tab.has-focus { 354 | transform: none; 355 | } 356 | 357 | .tab .tab_close { 358 | margin-left: 0.5em; 359 | width: 1.3rem; 360 | height: 1.3rem; 361 | padding: 0; 362 | text-align: center; 363 | vertical-align: middle; 364 | cursor: pointer; 365 | } 366 | 367 | .tab .tab_close:not(:hover):not(:focus) { 368 | -webkit-appearance: none; 369 | border: 0; 370 | background: none; 371 | color: grey; 372 | } 373 | 374 | cmenu h2 { 375 | font-size: 1.1em; 376 | margin: 0.1em 0; 377 | padding: 0 0.5em; 378 | } 379 | 380 | cmenu { 381 | font-family: inherit !important; 382 | } -------------------------------------------------------------------------------- /static/sw-wrapper.js: -------------------------------------------------------------------------------- 1 | /* global importScripts */ 2 | importScripts('/sw-toolbox.js'); 3 | importScripts('/scripts/sw-v1.js'); --------------------------------------------------------------------------------