├── .gitignore ├── .jshintrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── appdmg.json ├── background.png ├── bower.json ├── browser.js ├── desktop.css ├── desktop.html ├── desktop.js ├── editor.css ├── example.squiffy ├── gulpfile.js ├── index.html ├── main.js ├── menu.js ├── package.json ├── server.js ├── setup.iss ├── squiffy-editor.js ├── squiffy.icns ├── squiffy.ico ├── squiffy.png └── storage.js /.gitignore: -------------------------------------------------------------------------------- 1 | Squiffy.app 2 | Squiffy-win32-ia32 3 | Squiffy-linux-x64 4 | Squiffy-darwin-x64 5 | Output 6 | build 7 | dist 8 | *.dmg 9 | 10 | # Logs 11 | logs 12 | *.log 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directory 32 | # Commenting this out is preferred by some people, see 33 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 34 | node_modules 35 | bower_components 36 | 37 | # Users Environment Variables 38 | .lock-wscript 39 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 6 3 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "editor.tabSize": 2, 4 | "editor.insertSpaces": true, 5 | "search.exclude": { 6 | "build": true, 7 | "Squiffy-darwin-x64": true, 8 | "Squiffy-linux": true, 9 | "node_modules": true, 10 | "bower_components": true 11 | } 12 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 textadventures.co.uk 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Squiffy Editor (Archived) 2 | 3 | This repository contained the code for the Squiffy Editor. This has now been merged into the main Squiffy repository: https://github.com/textadventures/squiffy 4 | -------------------------------------------------------------------------------- /appdmg.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Squiffy", 3 | "icon": "squiffy.icns", 4 | "background": "background.png", 5 | "icon-size": 80, 6 | "contents": [ 7 | { "x": 150, "y": 100, "type": "file", "path": "Squiffy-darwin-x64/Squiffy.app" }, 8 | { "x": 250, "y": 100, "type": "link", "path": "/Applications" } 9 | ] 10 | } -------------------------------------------------------------------------------- /background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textadventures/squiffy-editor/460f263a7dd5d80daaac8c41d05ee64eaf9de0c8/background.png -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "squiffy-editor", 3 | "version": "0.0.0", 4 | "homepage": "https://github.com/textadventures/squiffy-editor", 5 | "authors": [ 6 | "Alex Warren " 7 | ], 8 | "license": "MIT", 9 | "ignore": [ 10 | "**/.*", 11 | "node_modules", 12 | "bower_components", 13 | "test", 14 | "tests" 15 | ], 16 | "dependencies": { 17 | "bootstrap": "~3.3.5", 18 | "jquery": "~2.1.4", 19 | "jquery-ui-layout-bower": "~1.4.4", 20 | "jquery-ui": "~1.11.3", 21 | "bootbox": "~4.4.0", 22 | "ace-builds": "~1.1.9", 23 | "chosen": "~1.4.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | var compile = function (input) { 3 | var url = 'http://squiffy.textadventures.co.uk'; 4 | 5 | if (input.zip) { 6 | // Using XMLHttpRequest here as jQuery doesn't support blob downloads 7 | url += '/zip'; 8 | var xhr = new XMLHttpRequest(); 9 | xhr.open('POST', url, true); 10 | xhr.responseType = 'blob'; 11 | 12 | xhr.onload = function (e) { 13 | if (this.status == 200) { 14 | input.success(this.response); 15 | } 16 | else { 17 | input.fail(this.response); 18 | } 19 | }; 20 | 21 | xhr.send(input.data); 22 | return; 23 | } 24 | 25 | $.ajax({ 26 | url: url, 27 | data: input.data, 28 | type: "POST", 29 | success: function (data) { 30 | if (data.indexOf('Failed') === 0) { 31 | input.fail(data); 32 | return; 33 | } 34 | input.success(data); 35 | }, 36 | error: function (xhr, status, err) { 37 | input.fail({ 38 | message: err 39 | }); 40 | } 41 | }); 42 | }; 43 | 44 | var userSettings = { 45 | get: function (setting) { 46 | var value = localStorage.getItem(setting); 47 | if (value === null) return null; 48 | return JSON.parse(value); 49 | }, 50 | set: function (setting, value) { 51 | localStorage.setItem(setting, JSON.stringify(value)); 52 | } 53 | }; 54 | 55 | var init = function (data, storageKey) { 56 | setTimeout(function () { 57 | $('#squiffy-editor').squiffyEditor({ 58 | data: data, 59 | compile: compile, 60 | autoSave: function () { 61 | }, 62 | storageKey: storageKey || 'squiffy', 63 | updateTitle: function (title) { 64 | document.title = title + ' - Squiffy Editor'; 65 | }, 66 | download: true, 67 | userSettings: userSettings 68 | }); 69 | }, 1); 70 | }; 71 | 72 | var saved = localStorage.squiffy; 73 | if (saved) { 74 | init(localStorage.squiffy); 75 | } else { 76 | init('@title New Game\n\n' + 77 | 'Start writing! You can delete all of this text, or play around with it if you\'re new to Squiffy.\n\n' + 78 | 'Each choice is represented by a [[new section]]. You can create links to new sections by surrounding them ' + 79 | 'in double square brackets.\n\n' + 80 | '[[new section]]:\n\nIn addition to sections, Squiffy has the concept of passages. These are sections of ' + 81 | 'text that don\'t advance the story. Passage links use single square brackets. For example, you can click this [passage link], and this [other passage ' + 82 | 'link], but the story won\'t advance until you click this [[section link]].\n\n' + 83 | '[passage link]:\n\nThis is the text for the first passage link.\n\n' + 84 | '[other passage link]:\n\nThis is the text for the second passage link.\n\n' + 85 | '[[section link]]:\n\nWhen a new section appears, any unclicked passage links from the previous section are disabled.'); 86 | } 87 | 88 | }); -------------------------------------------------------------------------------- /desktop.css: -------------------------------------------------------------------------------- 1 | #squiffy-logo { 2 | max-width: 60px; 3 | } 4 | 5 | #about-versions { 6 | font-size: 80%; 7 | } -------------------------------------------------------------------------------- /desktop.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Squiffy 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
32 | 33 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /desktop.js: -------------------------------------------------------------------------------- 1 | /* global __dirname */ 2 | /* global $ */ 3 | /* global process */ 4 | 5 | $(function () { 6 | const compiler = require('squiffy/compiler.js'); 7 | const remote = require('electron').remote; 8 | const shell = remote.shell; 9 | const path = require('path'); 10 | const dialog = remote.dialog; 11 | const fs = require('fs'); 12 | const clipboard = remote.clipboard; 13 | const storage = require('./storage.js'); 14 | 15 | const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json')).toString()); 16 | const editorVersion = packageJson.version; 17 | 18 | window.menuClick = window.menuClick || {}; 19 | 20 | var filename = null; 21 | var dirty = false; 22 | 23 | const compile = function (input) { 24 | var msgs = ""; 25 | 26 | console._log = console.log; 27 | console.log = function(msg) { 28 | msgs += "

" + msg + "

"; 29 | console._log(msg); 30 | }; 31 | 32 | const js = compiler.getJs(input.data); 33 | console.log = console._log; 34 | 35 | if (js.indexOf('Failed') === 0) { 36 | input.fail(js, msgs); 37 | return; 38 | } 39 | input.success(js, msgs); 40 | }; 41 | 42 | const build = function () { 43 | window.menuClick.saveFile(); 44 | if (dirty) return; 45 | 46 | var options = { 47 | write: true, 48 | escritorio: true, 49 | }; 50 | 51 | var result = compiler.generate(filename, options); 52 | 53 | if (result) { 54 | shell.openItem(path.join(result, 'index.html')); 55 | } 56 | else { 57 | dialog.showMessageBox({ 58 | type: 'warning', 59 | message: 'Failed to build script', 60 | buttons: ['OK'] 61 | }); 62 | } 63 | }; 64 | 65 | const setFilename = function (newFilename, noStore) { 66 | filename = newFilename; 67 | if (!noStore) localStorage['squiffy-filename'] = filename; 68 | if (!filename) { 69 | document.title = 'New file'; 70 | } 71 | else { 72 | document.title = path.basename(filename); 73 | remote.getCurrentWindow().setRepresentedFilename(filename); 74 | } 75 | if (process.platform != 'darwin') { 76 | document.title = document.title + ' - Squiffy'; 77 | } 78 | }; 79 | 80 | const setDirty = function (isDirty) { 81 | remote.getCurrentWindow().setDocumentEdited(isDirty); 82 | dirty = isDirty; 83 | }; 84 | 85 | const checkForUnsavedChanges = function () { 86 | if (!dirty) return true; 87 | 88 | var result = dialog.showMessageBox({ 89 | type: 'warning', 90 | buttons: ['Yes', 'No', 'Cancel'], 91 | message: 'Do you wish to save your changes to ' + (filename ? path.basename(filename) : 'this file') + '?' 92 | }); 93 | 94 | if (result === 0) { 95 | window.menuClick.saveFile(); 96 | return !dirty; 97 | } 98 | 99 | return (result !== 2); 100 | }; 101 | 102 | window.onbeforeunload = function (e) { 103 | return checkForUnsavedChanges(); 104 | }; 105 | 106 | setFilename(null, true); 107 | 108 | const loadFileData = function (file) { 109 | var data; 110 | try { 111 | data = fs.readFileSync(file).toString(); 112 | } 113 | catch (e) { 114 | return null; 115 | } 116 | setFilename(file); 117 | return data; 118 | }; 119 | 120 | const saveFile = function () { 121 | fs.writeFileSync(filename, $('#squiffy-editor').squiffyEditor('save')); 122 | $('#squiffy-editor').squiffyEditor('setInfo', 'Saved'); 123 | setDirty(false); 124 | }; 125 | 126 | window.menuClick.newFile = function () { 127 | if (!checkForUnsavedChanges()) return; 128 | $('#squiffy-editor').squiffyEditor('load', ''); 129 | setFilename(null); 130 | setDirty(false); 131 | }; 132 | 133 | window.menuClick.openFile = function () { 134 | if (!checkForUnsavedChanges()) return; 135 | const result = dialog.showOpenDialog({ 136 | filters: [ 137 | { name: 'Squiffy scripts', extensions: ['sq', 'squiffy'] } 138 | ] 139 | }); 140 | if (!result) return; 141 | window.loadFile(result[0]); 142 | }; 143 | 144 | window.loadFile = function (file) { 145 | if (!checkForUnsavedChanges()) return; 146 | const data = loadFileData(file); 147 | if (data === null) { 148 | dialog.showMessageBox({ 149 | type: 'warning', 150 | message: 'Failed to load file', 151 | buttons: ['OK'] 152 | }); 153 | } 154 | setDirty(false); 155 | $('#squiffy-editor').squiffyEditor('load', data); 156 | }; 157 | 158 | window.menuClick.saveFile = function () { 159 | if (!filename) { 160 | window.menuClick.saveFileAs(); 161 | return; 162 | } 163 | saveFile(); 164 | }; 165 | 166 | window.menuClick.saveFileAs = function () { 167 | const result = dialog.showSaveDialog({ 168 | filters: [ 169 | { name: 'Squiffy scripts', extensions: ['sq', 'squiffy'] } 170 | ] 171 | }); 172 | if (!result) return; 173 | setFilename(result); 174 | saveFile(); 175 | }; 176 | 177 | window.menuClick.undo = function () { 178 | $('#squiffy-editor').squiffyEditor('undo'); 179 | }; 180 | 181 | window.menuClick.redo = function () { 182 | $('#squiffy-editor').squiffyEditor('redo'); 183 | }; 184 | 185 | window.menuClick.cut = function () { 186 | clipboard.writeText($('#squiffy-editor').squiffyEditor('cut')); 187 | }; 188 | 189 | window.menuClick.copy = function () { 190 | clipboard.writeText($('#squiffy-editor').squiffyEditor('copy')); 191 | }; 192 | 193 | window.menuClick.paste = function () { 194 | $('#squiffy-editor').squiffyEditor('paste', clipboard.readText()); 195 | }; 196 | 197 | window.menuClick.selectAll = function () { 198 | $('#squiffy-editor').squiffyEditor('selectAll'); 199 | }; 200 | 201 | window.menuClick.run = function () { 202 | $('#squiffy-editor').squiffyEditor('run'); 203 | }; 204 | 205 | window.menuClick.find = function () { 206 | $('#squiffy-editor').squiffyEditor('find'); 207 | }; 208 | 209 | window.menuClick.replace = function () { 210 | $('#squiffy-editor').squiffyEditor('replace'); 211 | }; 212 | 213 | window.menuClick.build = function () { 214 | build(); 215 | }; 216 | 217 | window.menuClick.openFolder = function () { 218 | shell.showItemInFolder(filename); 219 | }; 220 | 221 | window.menuClick.documentation = function() { 222 | shell.openExternal('http://docs.textadventures.co.uk/squiffy/'); 223 | }; 224 | 225 | window.menuClick.about = function () { 226 | $('#about-build').text(editorVersion); 227 | var versions = []; 228 | for (var key in process.versions) { 229 | versions.push(key + ' ' + process.versions[key]); 230 | } 231 | $('#about-versions').text(versions.join(', ')); 232 | $('#about').modal(); 233 | }; 234 | 235 | window.menuClick.settings = function () { 236 | $('#settings-dialog').modal(); 237 | }; 238 | 239 | var userSettings = { 240 | get: function (setting) { 241 | return storage.get(setting); 242 | }, 243 | set: function (setting, value) { 244 | storage.set(setting, value); 245 | } 246 | }; 247 | 248 | var init = function (data) { 249 | $('#squiffy-editor').squiffyEditor({ 250 | data: data, 251 | desktop: true, 252 | compile: compile, 253 | open: window.menuClick.openFile, 254 | save: window.menuClick.saveFile, 255 | autoSave: function () {}, 256 | updateTitle: function () {}, 257 | setDirty: function () { 258 | setDirty(true); 259 | }, 260 | build: function () { 261 | build(); 262 | }, 263 | userSettings: userSettings 264 | }); 265 | }; 266 | 267 | var openFile = remote.getCurrentWindow().openFile; 268 | 269 | if (!openFile) { 270 | openFile = localStorage['squiffy-filename']; 271 | } 272 | 273 | if (openFile) { 274 | var data = loadFileData(openFile); 275 | if (data) { 276 | init(data); 277 | } 278 | else { 279 | openFile = null; 280 | } 281 | } 282 | 283 | if (!openFile) { 284 | $.get('example.squiffy', init); 285 | } 286 | 287 | $('#squiffy-editor').on('click', 'a.external-link, #output-container a[href]', function (e) { 288 | shell.openExternal($(this).attr('href')); 289 | e.preventDefault(); 290 | }); 291 | 292 | $('#update-check').click(function () { 293 | $('#about-update').text('Checking for updates...'); 294 | $.get('http://textadventures.co.uk/squiffy/versioncheck/?version=' + editorVersion, function (result) { 295 | if (result.Latest <= editorVersion) { 296 | $('#about-update').text('You are running the latest version of Squiffy.'); 297 | } 298 | else { 299 | $('#about-update').html('New version! Update to ' + result.Name + ''); 300 | $('#update-link').click(function () { 301 | shell.openExternal(result.Url); 302 | }); 303 | } 304 | }); 305 | }); 306 | 307 | document.addEventListener('dragover', function (event) { 308 | event.preventDefault(); 309 | return false; 310 | }, false); 311 | 312 | document.addEventListener('drop', function (event) { 313 | event.preventDefault(); 314 | return false; 315 | }, false); 316 | }); 317 | -------------------------------------------------------------------------------- /editor.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | overflow: hidden; 4 | } 5 | 6 | #squiffy-editor { 7 | position: absolute; 8 | top: 0; 9 | bottom: 0; 10 | left: 0; 11 | right: 0; 12 | overflow: auto; 13 | } 14 | 15 | a.squiffy-link 16 | { 17 | text-decoration: underline; 18 | color: Blue; 19 | cursor: pointer; 20 | } 21 | 22 | a.squiffy-link.disabled 23 | { 24 | text-decoration: inherit; 25 | color: inherit !important; 26 | cursor: inherit; 27 | } 28 | 29 | #editor { 30 | position: absolute; 31 | top: 0; 32 | right: 0; 33 | bottom: 0; 34 | left: 0; 35 | } 36 | 37 | #debugger { 38 | position: absolute; 39 | top: 0; 40 | right: 0; 41 | bottom: 0; 42 | left: 0; 43 | overflow-y: scroll; 44 | font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace; 45 | } 46 | 47 | #squiffy-editor { 48 | width: 100%; 49 | height: 100%; 50 | position: fixed; 51 | } 52 | 53 | #output, #tab-help, #tab-tools { 54 | position: absolute; 55 | top: 54px; 56 | right: 0; 57 | bottom: 0; 58 | left: 0; 59 | padding: 20px; 60 | background: #fff; 61 | } 62 | 63 | .ui-layout-north { 64 | z-index: 50 !important; 65 | overflow: visible !important; 66 | -webkit-user-select: none; 67 | } 68 | 69 | .ui-layout-center { 70 | padding: 0px !important; 71 | } 72 | 73 | #info { 74 | margin-left: 20px; 75 | font-size: 90%; 76 | } 77 | 78 | ul.dropdown-menu a { 79 | cursor: pointer; 80 | } 81 | 82 | .toolbar { 83 | margin-top: 10px; 84 | } 85 | 86 | .tab-pane h3:first-of-type { 87 | margin-top: 0; 88 | } 89 | 90 | .tab-pane h3:not(:first-of-type) { 91 | margin-top: 30px; 92 | } 93 | 94 | .ace-squiffy .ace_gutter { 95 | background: #ebebeb; 96 | color: #333; 97 | overflow: hidden; 98 | } 99 | 100 | .ace-squiffy .ace_print-margin { 101 | width: 1px; 102 | background: #e8e8e8; 103 | } 104 | 105 | .ace-squiffy { 106 | background-color: #FFFFFF; 107 | color: black; 108 | } 109 | 110 | .ace-squiffy .ace_identifier { 111 | color: black; 112 | } 113 | 114 | .ace-squiffy .ace_keyword { 115 | color: #0000FF; 116 | } 117 | 118 | .ace-squiffy .ace_numeric { 119 | color: black; 120 | } 121 | 122 | .ace-squiffy .ace_storage { 123 | color: #11B7BE; 124 | } 125 | 126 | .ace-squiffy .ace_keyword.ace_operator, 127 | .ace-squiffy .ace_lparen, 128 | .ace-squiffy .ace_rparen, 129 | .ace-squiffy .ace_punctuation { 130 | color: #808080; 131 | } 132 | 133 | .ace-squiffy .ace_set.ace_statement { 134 | color: #0000FF; 135 | text-decoration: underline; 136 | } 137 | 138 | .ace-squiffy .ace_cursor { 139 | color: black; 140 | } 141 | 142 | .ace-squiffy .ace_invisible { 143 | color: rgb(191, 191, 191); 144 | } 145 | 146 | .ace-squiffy .ace_constant.ace_buildin { 147 | color: rgb(88, 72, 246); 148 | } 149 | 150 | .ace-squiffy .ace_constant.ace_language { 151 | color: #979797; 152 | } 153 | 154 | .ace-squiffy .ace_constant.ace_library { 155 | color: rgb(6, 150, 14); 156 | } 157 | 158 | .ace-squiffy .ace_invalid { 159 | background-color: rgb(153, 0, 0); 160 | color: white; 161 | } 162 | 163 | .ace-squiffy .ace_support.ace_function { 164 | color: #FF00FF; 165 | } 166 | 167 | .ace-squiffy .ace_support.ace_constant { 168 | color: rgb(6, 150, 14); 169 | } 170 | 171 | .ace-squiffy .ace_class { 172 | color: #008080; 173 | } 174 | 175 | .ace-squiffy .ace_support.ace_other { 176 | color: #6D79DE; 177 | } 178 | 179 | .ace-squiffy .ace_variable.ace_parameter { 180 | font-style: italic; 181 | color: #FD971F; 182 | } 183 | 184 | .ace-squiffy .ace_comment { 185 | color: #008000; 186 | } 187 | 188 | .ace-squiffy .ace_constant.ace_numeric { 189 | color: black; 190 | } 191 | 192 | .ace-squiffy .ace_variable { 193 | color: rgb(49, 132, 149); 194 | } 195 | 196 | .ace-squiffy .ace_xml-pe { 197 | color: rgb(104, 104, 91); 198 | } 199 | 200 | .ace-squiffy .ace_support.ace_storedprocedure { 201 | color: #800000; 202 | } 203 | 204 | .ace-squiffy .ace_heading { 205 | color: rgb(6, 150, 14); 206 | } 207 | 208 | .ace-squiffy .ace_heading.ace_section { 209 | font-weight: bold; 210 | } 211 | 212 | .ace-squiffy .ace_list { 213 | color: rgb(185, 6, 144); 214 | } 215 | 216 | .ace-squiffy .ace_marker-layer .ace_selection { 217 | background: rgb(181, 213, 255); 218 | } 219 | 220 | .ace-squiffy .ace_marker-layer .ace_step { 221 | background: rgb(252, 255, 0); 222 | } 223 | 224 | .ace-squiffy .ace_marker-layer .ace_stack { 225 | background: rgb(164, 229, 101); 226 | } 227 | 228 | .ace-squiffy .ace_marker-layer .ace_bracket { 229 | margin: -1px 0 0 -1px; 230 | border: 1px solid rgb(192, 192, 192); 231 | } 232 | 233 | .ace-squiffy .ace_marker-layer .ace_active-line { 234 | background: rgba(0, 0, 0, 0.07); 235 | } 236 | 237 | .ace-squiffy .ace_gutter-active-line { 238 | background-color: #dcdcdc; 239 | } 240 | 241 | .ace-squiffy .ace_marker-layer .ace_selected-word { 242 | background: rgb(250, 250, 255); 243 | border: 1px solid rgb(200, 200, 250); 244 | } 245 | 246 | .ace-squiffy .ace_meta.ace_tag { 247 | color: #0000FF; 248 | } 249 | 250 | .ace-squiffy .ace_string.ace_regex { 251 | color: #FF0000; 252 | } 253 | 254 | .ace-squiffy .ace_string { 255 | color: #FF0000; 256 | } 257 | 258 | .ace-squiffy .ace_entity.ace_other.ace_attribute-name { 259 | color: #994409; 260 | } 261 | 262 | .ace-squiffy .ace_indent-guide { 263 | background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==") right repeat-y; 264 | } -------------------------------------------------------------------------------- /example.squiffy: -------------------------------------------------------------------------------- 1 | Squiffy is a tool for creating interactive stories. 2 | 3 | You write your story in a text file. Each choice is represented by a [[new section]]. You can create links to new sections by surrounding them in double square brackets. 4 | 5 | [[new section]]: 6 | 7 | In addition to sections, Squiffy has the concept of passages. These are sections of text that don't advance the story. For example, you can click this [passage link], and this [other passage link], but the story won't advance until you click this [[section link]]. 8 | 9 | [passage link]: 10 | 11 | This is the text for the first passage link. 12 | 13 | [other passage link]: 14 | 15 | This is the text for the second passage link. 16 | 17 | [[section link]]: 18 | 19 | When a new section appears, any unclicked passage links from the previous section are disabled. -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* jshint esversion: 6 */ 2 | 3 | var gulp = require('gulp'); 4 | var del = require('del'); 5 | var install = require('gulp-install'); 6 | var packager = require('electron-packager'); 7 | var rename = require('gulp-rename'); 8 | var shell = require('gulp-shell'); 9 | var jshint = require('gulp-jshint'); 10 | 11 | var electronVersion = '1.8.1'; 12 | var squiffyVersion = '5.1.3'; 13 | 14 | gulp.task('lint', function() { 15 | return gulp.src('./*.js') 16 | .pipe(jshint()) 17 | .pipe(jshint.reporter('jshint-stylish')) 18 | .pipe(jshint.reporter('fail')); 19 | }); 20 | 21 | gulp.task('clean', ['lint'], function() { 22 | return del(['dist']); 23 | }); 24 | 25 | gulp.task('clean-osx', function() { 26 | return del(['Squiffy-darwin-x64', 'Squiffy.dmg']); 27 | }); 28 | 29 | gulp.task('clean-linux', function() { 30 | return del(['Squiffy-linux-x64']); 31 | }); 32 | 33 | gulp.task('clean-windows', function() { 34 | return del(['Squiffy-win32-ia32', 'Output']); 35 | }); 36 | 37 | gulp.task('package.json', ['clean'], function () { 38 | return gulp.src('package.json') 39 | .pipe(gulp.dest('dist')); 40 | }); 41 | 42 | gulp.task('modules', ['clean', 'package.json'], function () { 43 | return gulp.src(['dist/package.json']) 44 | .pipe(install({production: true})); 45 | }); 46 | 47 | gulp.task('bootstrap', ['clean'], function () { 48 | return gulp.src('bower_components/bootstrap/dist/**/*') 49 | .pipe(gulp.dest('dist/bower_components/bootstrap/dist')); 50 | }); 51 | 52 | gulp.task('jquery', ['clean'], function () { 53 | return gulp.src('node_modules/jquery/dist/*') 54 | .pipe(gulp.dest('node_modules/jquery/dist')); 55 | }); 56 | 57 | gulp.task('ace', ['clean'], function () { 58 | return gulp.src('bower_components/ace-builds/src-min/**/*') 59 | .pipe(gulp.dest('dist/bower_components/ace-builds/src-min')); 60 | }); 61 | 62 | gulp.task('jquery-ui', ['clean'], function () { 63 | return gulp.src('bower_components/jquery-ui/jquery-ui.min.js') 64 | .pipe(gulp.dest('dist/bower_components/jquery-ui')); 65 | }); 66 | 67 | gulp.task('jquery-ui-layout', ['clean'], function () { 68 | return gulp.src('bower_components/jquery-ui-layout-bower/source/stable/jquery.layout.min.js') 69 | .pipe(gulp.dest('dist/bower_components/jquery-ui-layout-bower/source/stable')); 70 | }); 71 | 72 | gulp.task('bootbox', ['clean'], function () { 73 | return gulp.src('bower_components/bootbox/bootbox.js') 74 | .pipe(gulp.dest('dist/bower_components/bootbox')); 75 | }); 76 | 77 | gulp.task('chosen', ['clean'], function () { 78 | return gulp.src('bower_components/chosen/**/*') 79 | .pipe(gulp.dest('dist/bower_components/chosen')); 80 | }); 81 | 82 | gulp.task('build-common', ['modules', 'bootstrap', 'jquery', 'ace', 'jquery-ui', 'jquery-ui-layout', 'bootbox', 'chosen'], function () { 83 | return gulp.src([ 84 | 'desktop.*', 85 | 'editor.css', 86 | 'example.squiffy', 87 | 'desktop.html', 88 | '*.js', 89 | 'squiffy.png', 90 | ]) 91 | .pipe(gulp.dest('dist')); 92 | }); 93 | 94 | /* 95 | electron-packager supports this option: 96 | --sign="Developer ID Application: Luis Felipe Morales (2PZ4BV3S43)" 97 | but we're hacking in a custom Info.plist so we sign afterwards 98 | */ 99 | 100 | gulp.task('osx', ['build-common', 'clean-osx'], function (callback) { 101 | var options = { 102 | dir: './dist', 103 | name: 'Squiffy', 104 | platform: 'darwin', 105 | arch: 'x64', 106 | version: electronVersion, 107 | 'app-bundle-id': 'uk.co.textadventures.squiffy', 108 | 'helper-bundle-id': 'uk.co.textadventures.squiffy.helper', 109 | icon: 'squiffy.icns', 110 | 'app-version': squiffyVersion 111 | }; 112 | 113 | packager(options, function (err, appPath) { 114 | if (err) return console.log(err); 115 | callback(); 116 | }); 117 | }); 118 | 119 | 120 | gulp.task('linux', ['build-common', 'clean-linux'], function (callback) { 121 | var options = { 122 | dir: './dist', 123 | name: 'Squiffy', 124 | platform: 'linux', 125 | arch: 'x64', 126 | version: electronVersion, 127 | 'app-bundle-id': 'uk.co.textadventures.squiffy', 128 | 'helper-bundle-id': 'uk.co.textadventures.squiffy.helper', 129 | 'app-version': squiffyVersion 130 | }; 131 | 132 | packager(options, function (err, appPath) { 133 | if (err) return console.log(err); 134 | callback(); 135 | }); 136 | }); 137 | 138 | gulp.task('windows', ['build-common', 'clean-windows'], function (callback) { 139 | var options = { 140 | dir: './dist', 141 | name: 'Squiffy', 142 | platform: 'win32', 143 | arch: 'ia32', 144 | version: electronVersion, 145 | 'app-bundle-id': 'uk.co.textadventures.squiffy', 146 | 'helper-bundle-id': 'uk.co.textadventures.squiffy.helper', 147 | icon: 'squiffy.ico', 148 | 'app-version': squiffyVersion, 149 | ignore: 'Output', 150 | 'version-string': { 151 | 'ProductName': 'Squiffy', 152 | 'FileDescription': 'Squiffy', 153 | 'LegalCopyright': 'Copyright (c) 2017 Luis Felipe Morales', 154 | 'OriginalFilename': 'Squiffy.exe', 155 | 'FileVersion': squiffyVersion, 156 | 'ProductVersion': squiffyVersion, 157 | 'InternalName': 'Squiffy', 158 | 'CompanyName': 'Luis Felipe Morales' 159 | } 160 | }; 161 | 162 | packager(options, function (err, appPath) { 163 | if (err) return console.log(err); 164 | callback(); 165 | }); 166 | }); 167 | 168 | gulp.task('osx-file-assoc', ['osx'], function () { 169 | var plist = require('plist'); 170 | var fs = require('fs'); 171 | var obj = plist.parse(fs.readFileSync('Squiffy-darwin-x64/Squiffy.app/Contents/Info.plist', 'utf8')); 172 | 173 | obj.CFBundleDocumentTypes = [{ 174 | 'CFBundleTypeExtensions':['squiffy'], 175 | 'CFBundleTypeIconFile':'atom', 176 | 'CFBundleTypeName':'Squiffy project', 177 | 'CFBundleTypeRole':'Editor', 178 | 'CFBundleTypeOSTypes':[ 179 | 'TEXT','utxt','TUTX','****' 180 | ]}]; 181 | 182 | fs.writeFileSync('Squiffy-darwin-x64/Squiffy.app/Contents/Info.plist', 183 | plist.build(obj), 184 | 'utf8'); 185 | }); 186 | 187 | gulp.task('osx-verify-file-assoc', ['osx-file-assoc'], shell.task([ 188 | '/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -lint -f Squiffy-darwin-x64/Squiffy.app' 189 | ])); 190 | 191 | gulp.task('osx-sign-clean', ['osx-verify-file-assoc'], function () { 192 | return del([ 193 | 'Squiffy-darwin-x64/Squiffy.app/Contents/Frameworks/Squiffy Helper.app/Contents/MacOS/Squiffy Helper.cstemp', 194 | 'Squiffy-darwin-x64/Squiffy.app/Contents/Frameworks/Squiffy Helper EH.app/Contents/MacOS/Squiffy Helper EH.cstemp', 195 | 'Squiffy-darwin-x64/Squiffy.app/Contents/Frameworks/Squiffy Helper NP.app/Contents/MacOS/Squiffy Helper NP.cstemp' 196 | ]); 197 | }); 198 | 199 | gulp.task('osx-sign', ['osx-sign-clean'], shell.task([ 200 | 'codesign -s "Developer ID Application: Luis Felipe Morales (2PZ4BV3S43)" Squiffy-darwin-x64/Squiffy.app/Contents/Frameworks/Squiffy\\ Helper.app', 201 | 'codesign -s "Developer ID Application: Luis Felipe Morales (2PZ4BV3S43)" Squiffy-darwin-x64/Squiffy.app/Contents/Frameworks/Squiffy\\ Helper\\ EH.app/Contents/MacOS/Squiffy\\ Helper\\ EH', 202 | 'codesign -s "Developer ID Application: Luis Felipe Morales (2PZ4BV3S43)" Squiffy-darwin-x64/Squiffy.app/Contents/Frameworks/Squiffy\\ Helper\\ EH.app', 203 | 'codesign -s "Developer ID Application: Luis Felipe Morales (2PZ4BV3S43)" Squiffy-darwin-x64/Squiffy.app/Contents/Frameworks/Squiffy\\ Helper\\ NP.app/Contents/MacOS/Squiffy\\ Helper\\ NP', 204 | 'codesign -s "Developer ID Application: Luis Felipe Morales (2PZ4BV3S43)" Squiffy-darwin-x64/Squiffy.app/Contents/Frameworks/Squiffy\\ Helper\\ NP.app', 205 | 'codesign -s "Developer ID Application: Luis Felipe Morales (2PZ4BV3S43)" Squiffy-darwin-x64/Squiffy.app/Contents/Frameworks/Electron\\ Framework.framework/Versions/Current/Electron\\ Framework', 206 | 'codesign -s "Developer ID Application: Luis Felipe Morales (2PZ4BV3S43)" Squiffy-darwin-x64/Squiffy.app/Contents/Frameworks/Mantle.framework', 207 | 'codesign -s "Developer ID Application: Luis Felipe Morales (2PZ4BV3S43)" Squiffy-darwin-x64/Squiffy.app/Contents/Frameworks/ReactiveCocoa.framework', 208 | 'codesign -s "Developer ID Application: Luis Felipe Morales (2PZ4BV3S43)" Squiffy-darwin-x64/Squiffy.app/Contents/Frameworks/Squirrel.framework', 209 | 'codesign -s "Developer ID Application: Luis Felipe Morales (2PZ4BV3S43)" Squiffy-darwin-x64/Squiffy.app', 210 | 'spctl --verbose=4 --assess --type execute Squiffy-darwin-x64/Squiffy.app' 211 | ])); 212 | 213 | gulp.task('osx-dmg', ['osx-sign'], function () { 214 | var appdmg = require('appdmg'); 215 | appdmg({ 216 | source: 'appdmg.json', 217 | target: `Squiffy.${squiffyVersion}.OSX.dmg` 218 | }); 219 | }); 220 | 221 | gulp.task('windows-setup', ['windows'], function () { 222 | var innosetup = require('innosetup-compiler'); 223 | innosetup('setup.iss', { 224 | verbose: true 225 | }); 226 | }); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Squiffy 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /* global __dirname */ 2 | /* global process */ 3 | 4 | var electron = require('electron'); 5 | var app = electron.app; // Module to control application life. 6 | var BrowserWindow = electron.BrowserWindow; // Module to create native browser window. 7 | var storage = require('./storage'); 8 | 9 | // Report crashes to our server. 10 | electron.crashReporter.start({ 11 | companyName: "textadventure.co.uk", 12 | submitURL: "https://github.com/textadventures/squiffy-editor/issues", 13 | }); 14 | 15 | var argv = process.argv; 16 | 17 | var openFile; 18 | 19 | if (process.platform !== 'darwin') { 20 | openFile = process.argv[1]; 21 | } 22 | 23 | app.on('open-file', function (event, path) { 24 | event.preventDefault(); 25 | openFile = path; 26 | if (mainWindow) { 27 | mainWindow.webContents.executeJavaScript('loadFile(' + JSON.stringify(path) + ')'); 28 | } 29 | }); 30 | 31 | // Keep a global reference of the window object, if you don't, the window will 32 | // be closed automatically when the javascript object is GCed. 33 | var mainWindow = null; 34 | 35 | var init = function() { 36 | var lastWindowState = storage.get('lastWindowState'); 37 | if (lastWindowState === null) { 38 | lastWindowState = { 39 | width: 1200, 40 | height: 600, 41 | maximized: false 42 | }; 43 | } 44 | 45 | mainWindow = new BrowserWindow({ 46 | x: lastWindowState.x, 47 | y: lastWindowState.y, 48 | width: lastWindowState.width, 49 | height: lastWindowState.height, 50 | icon: __dirname + '/squiffy.png' 51 | }); 52 | 53 | if (lastWindowState.maximized) { 54 | mainWindow.maximize(); 55 | } 56 | 57 | mainWindow.openFile = openFile; 58 | 59 | // and load the index.html of the app. 60 | mainWindow.loadURL('file://' + __dirname + '/desktop.html'); 61 | 62 | mainWindow.on('close', function() { 63 | var bounds = mainWindow.getBounds(); 64 | storage.set('lastWindowState', { 65 | x: bounds.x, 66 | y: bounds.y, 67 | width: bounds.width, 68 | height: bounds.height, 69 | maximized: mainWindow.isMaximized() 70 | }); 71 | mainWindow.destroy(); 72 | }); 73 | 74 | // Emitted when the window is closed. 75 | mainWindow.on('closed', function() { 76 | // Dereference the window object, usually you would store windows 77 | // in an array if your app supports multi windows, this is the time 78 | // when you should delete the corresponding element. 79 | mainWindow = null; 80 | }); 81 | }; 82 | 83 | // Quit when all windows are closed, except on OS X. 84 | app.on('window-all-closed', function() { 85 | if (process.platform != 'darwin') { 86 | app.quit(); 87 | } 88 | }); 89 | 90 | // On OS X, this is called when the app is running in the Dock with no open windows. 91 | app.on('activate-with-no-open-windows', init); 92 | 93 | // This method will be called when Electron has done everything 94 | // initialization and ready for creating browser windows. 95 | app.on('ready', init); 96 | -------------------------------------------------------------------------------- /menu.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var remote = require('electron').remote; 3 | var app = remote.app; 4 | window.menuClick = window.menuClick || {}; 5 | 6 | var fileNew = function () { 7 | window.menuClick.newFile(); 8 | }; 9 | 10 | var fileOpen = function () { 11 | window.menuClick.openFile(); 12 | }; 13 | 14 | var fileSave = function () { 15 | window.menuClick.saveFile(); 16 | }; 17 | 18 | var fileSaveAs = function () { 19 | window.menuClick.saveFileAs(); 20 | }; 21 | 22 | var editUndo = function () { 23 | window.menuClick.undo(); 24 | }; 25 | 26 | var editRedo = function () { 27 | window.menuClick.redo(); 28 | }; 29 | 30 | var editCut = function () { 31 | window.menuClick.cut(); 32 | }; 33 | 34 | var editCopy = function () { 35 | window.menuClick.copy(); 36 | }; 37 | 38 | var editPaste = function () { 39 | window.menuClick.paste(); 40 | }; 41 | 42 | var editSelectAll = function () { 43 | window.menuClick.selectAll(); 44 | }; 45 | 46 | var editFind = function () { 47 | window.menuClick.find(); 48 | }; 49 | 50 | var editReplace = function () { 51 | window.menuClick.replace(); 52 | }; 53 | 54 | var documentation = function () { 55 | window.menuClick.documentation(); 56 | }; 57 | 58 | var buildRun = function () { 59 | window.menuClick.run(); 60 | }; 61 | 62 | var buildBuild = function () { 63 | window.menuClick.build(); 64 | }; 65 | 66 | var buildOpenFolder = function () { 67 | window.menuClick.openFolder(); 68 | }; 69 | 70 | var about = function () { 71 | window.menuClick.about(); 72 | }; 73 | 74 | var settings = function () { 75 | window.menuClick.settings(); 76 | }; 77 | 78 | var template; 79 | 80 | if (process.platform === 'darwin') { 81 | template = [ 82 | { 83 | label: 'Squiffy', 84 | submenu: [ 85 | { 86 | label: 'About Squiffy', 87 | click: about 88 | }, 89 | { 90 | type: 'separator' 91 | }, 92 | { 93 | label: 'Preferences...', 94 | accelerator: 'Command+,', 95 | click: settings 96 | }, 97 | { 98 | type: 'separator' 99 | }, 100 | { 101 | label: 'Services', 102 | submenu: [] 103 | }, 104 | { 105 | type: 'separator' 106 | }, 107 | { 108 | label: 'Hide Squiffy', 109 | accelerator: 'Command+H', 110 | selector: 'hide:' 111 | }, 112 | { 113 | label: 'Hide Others', 114 | accelerator: 'Command+Shift+H', 115 | selector: 'hideOtherApplications:' 116 | }, 117 | { 118 | label: 'Show All', 119 | selector: 'unhideAllApplications:' 120 | }, 121 | { 122 | type: 'separator' 123 | }, 124 | { 125 | label: 'Quit', 126 | accelerator: 'Command+Q', 127 | selector: 'terminate:' 128 | }, 129 | ] 130 | }, 131 | { 132 | label: 'File', 133 | submenu: [ 134 | { 135 | label: 'New', 136 | accelerator: 'Command+N', 137 | click: fileNew 138 | }, 139 | { 140 | label: 'Open...', 141 | accelerator: 'Command+O', 142 | click: fileOpen 143 | }, 144 | { 145 | label: 'Save', 146 | accelerator: 'Command+S', 147 | click: fileSave 148 | }, 149 | { 150 | label: 'Save As...', 151 | accelerator: 'Command+Shift+S', 152 | click: fileSaveAs 153 | }, 154 | ] 155 | }, 156 | { 157 | label: 'Edit', 158 | submenu: [ 159 | { 160 | label: 'Undo', 161 | accelerator: 'Command+Z', 162 | click: editUndo 163 | }, 164 | { 165 | label: 'Redo', 166 | accelerator: 'Command+Y', 167 | click: editRedo 168 | }, 169 | { 170 | type: 'separator' 171 | }, 172 | { 173 | label: 'Cut', 174 | accelerator: 'Command+X', 175 | click: editCut 176 | }, 177 | { 178 | label: 'Copy', 179 | accelerator: 'Command+C', 180 | click: editCopy 181 | }, 182 | { 183 | label: 'Paste', 184 | accelerator: 'Command+V', 185 | click: editPaste 186 | }, 187 | { 188 | label: 'Select All', 189 | accelerator: 'Command+A', 190 | click: editSelectAll 191 | }, 192 | { 193 | type: 'separator' 194 | }, 195 | { 196 | label: 'Find', 197 | accelerator: 'Command+F', 198 | click: editFind 199 | }, 200 | { 201 | label: 'Replace', 202 | accelerator: 'Alt+Command+F', 203 | click: editReplace 204 | }, 205 | { 206 | type: 'separator' 207 | }, 208 | { 209 | label: 'Settings', 210 | accelerator: null, 211 | click: settings 212 | } 213 | ] 214 | }, 215 | { 216 | label: 'View', 217 | submenu: [ 218 | { 219 | label: 'Reload', 220 | accelerator: 'Alt+Command+R', 221 | click: function() { remote.getCurrentWindow().reloadIgnoringCache(); } 222 | }, 223 | { 224 | label: 'Toggle DevTools', 225 | accelerator: 'Alt+Command+I', 226 | click: function() { remote.getCurrentWindow().toggleDevTools(); } 227 | }, 228 | ] 229 | }, 230 | { 231 | label: 'Build', 232 | submenu: [ 233 | { 234 | label: 'Run', 235 | accelerator: 'Command+R', 236 | click: buildRun 237 | }, 238 | { 239 | label: 'Build', 240 | accelerator: 'Command+B', 241 | click: buildBuild 242 | }, 243 | { 244 | label: 'Open Folder', 245 | click: buildOpenFolder 246 | } 247 | ] 248 | }, 249 | { 250 | label: 'Window', 251 | submenu: [ 252 | { 253 | label: 'Minimize', 254 | accelerator: 'Command+M', 255 | selector: 'performMiniaturize:' 256 | }, 257 | { 258 | label: 'Close', 259 | accelerator: 'Command+W', 260 | selector: 'performClose:' 261 | }, 262 | { 263 | type: 'separator' 264 | }, 265 | { 266 | label: 'Bring All to Front', 267 | selector: 'arrangeInFront:' 268 | }, 269 | ] 270 | }, 271 | { 272 | label: 'Help', 273 | submenu: [ 274 | { 275 | label: 'Documentation', 276 | click: documentation 277 | } 278 | ] 279 | }, 280 | ]; 281 | } 282 | else { 283 | template = [ 284 | { 285 | label: '&File', 286 | submenu: [ 287 | { 288 | label: '&New', 289 | accelerator: 'Ctrl+N', 290 | click: fileNew 291 | }, 292 | { 293 | label: '&Open...', 294 | accelerator: 'Ctrl+O', 295 | click: fileOpen 296 | }, 297 | { 298 | label: '&Save', 299 | accelerator: 'Ctrl+S', 300 | click: fileSave 301 | }, 302 | { 303 | label: 'Save &As...', 304 | accelerator: 'Ctrl+Shift+S', 305 | click: fileSaveAs 306 | }, 307 | { 308 | type: 'separator' 309 | }, 310 | { 311 | label: 'E&xit', 312 | click: function() { app.quit(); } 313 | } 314 | ] 315 | }, 316 | { 317 | label: '&Edit', 318 | submenu: [ 319 | { 320 | label: '&Undo', 321 | accelerator: 'Ctrl+Z', 322 | click: editUndo 323 | }, 324 | { 325 | label: '&Redo', 326 | accelerator: 'Ctrl+Y', 327 | click: editRedo 328 | }, 329 | { 330 | type: 'separator' 331 | }, 332 | { 333 | label: 'Cu&t', 334 | accelerator: 'Ctrl+X', 335 | click: editCut 336 | }, 337 | { 338 | label: '&Copy', 339 | accelerator: 'Ctrl+C', 340 | click: editCopy 341 | }, 342 | { 343 | label: '&Paste', 344 | accelerator: 'Ctrl+V', 345 | click: editPaste 346 | }, 347 | { 348 | label: 'Select &All', 349 | accelerator: 'Ctrl+A', 350 | click: editSelectAll 351 | }, 352 | { 353 | type: 'separator' 354 | }, 355 | { 356 | label: '&Find', 357 | accelerator: 'Ctrl+F', 358 | click: editFind 359 | }, 360 | { 361 | label: '&Replace', 362 | accelerator: 'Ctrl+H', 363 | click: editReplace 364 | }, 365 | { 366 | type: 'separator' 367 | }, 368 | { 369 | label: 'Settings', 370 | accelerator: null, 371 | click: settings 372 | } 373 | ] 374 | }, 375 | { 376 | label: '&View', 377 | submenu: [ 378 | { 379 | label: '&Reload', 380 | accelerator: 'Ctrl+Shift+R', 381 | click: function() { remote.getCurrentWindow().reloadIgnoringCache(); } 382 | }, 383 | { 384 | label: '&Toggle DevTools', 385 | accelerator: 'F12', 386 | click: function() { remote.getCurrentWindow().toggleDevTools(); } 387 | }, 388 | ] 389 | }, 390 | { 391 | label: '&Build', 392 | submenu: [ 393 | { 394 | label: '&Run', 395 | accelerator: 'Ctrl+R', 396 | click: buildRun 397 | }, 398 | { 399 | label: '&Build', 400 | accelerator: 'Ctrl+B', 401 | click: buildBuild 402 | }, 403 | { 404 | label: '&Open Folder', 405 | click: buildOpenFolder 406 | } 407 | ] 408 | }, 409 | { 410 | label: '&Help', 411 | submenu: [ 412 | { 413 | label: '&Documentation', 414 | click: documentation 415 | }, 416 | { 417 | type: 'separator' 418 | }, 419 | { 420 | label: '&About Squiffy', 421 | click: about 422 | } 423 | ] 424 | }, 425 | ]; 426 | } 427 | 428 | var Menu = remote.Menu; 429 | 430 | menu = Menu.buildFromTemplate(template); 431 | 432 | Menu.setApplicationMenu(menu); 433 | })(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "squiffy-editor", 3 | "version": "5.1.3", 4 | "description": "", 5 | "main": "main.js", 6 | "scripts": { 7 | "start": "electron .", 8 | "web": "node server.js" 9 | }, 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/textadventures/squiffy-editor.git" 14 | }, 15 | "devDependencies": { 16 | "del": "^2.2.0", 17 | "electron-packager": "^5.2.1", 18 | "electron": "^1.8.1", 19 | "finalhandler": "^0.4.0", 20 | "gulp": "^3.9.1", 21 | "gulp-install": "^0.6.0", 22 | "gulp-jshint": "^2.0.0", 23 | "gulp-rename": "^1.2.2", 24 | "gulp-shell": "^0.5.2", 25 | "innosetup-compiler": "^5.5.8", 26 | "jshint": "^2.9.1", 27 | "jshint-stylish": "^2.1.0", 28 | "plist": "^1.2.0", 29 | "serve-static": "^1.10.0", 30 | "jquery": "~2.1.4" 31 | }, 32 | "optionalDependencies": { 33 | "appdmg": "^0.3.5" 34 | }, 35 | "dependencies": { 36 | "squiffy": "^5.0.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | function startServer(dir, port) { 2 | var finalhandler = require('finalhandler'); 3 | var http = require('http'); 4 | var serveStatic = require('serve-static'); 5 | 6 | var serve = serveStatic(dir, { index: ['index.html'] }); 7 | 8 | var server = http.createServer(function(req, res){ 9 | var done = finalhandler(req, res); 10 | serve(req, res, done); 11 | }); 12 | 13 | server.listen(port); 14 | } 15 | 16 | var port = 8282; 17 | startServer(__dirname, port); 18 | console.log('Started http://localhost:' + port + '/'); -------------------------------------------------------------------------------- /setup.iss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textadventures/squiffy-editor/460f263a7dd5d80daaac8c41d05ee64eaf9de0c8/setup.iss -------------------------------------------------------------------------------- /squiffy-editor.js: -------------------------------------------------------------------------------- 1 | /* global ace */ 2 | /* global $ */ 3 | /* global define */ 4 | /* jshint quotmark: single */ 5 | /* jshint evil: true */ 6 | /* jshint multistr: true */ 7 | 8 | (function () { 9 | 'use strict'; 10 | 11 | String.prototype.format = function() { 12 | var args = arguments; 13 | return this.replace(/{(\d+)}/g, function(match, number) { 14 | return typeof args[number] != 'undefined' ? args[number] : match; 15 | }); 16 | }; 17 | 18 | var editor, settings, title, loading, layout, sourceMap, 19 | currentRow, currentSection, currentPassage; 20 | 21 | const defaultSettings = { 22 | fontSize: 12 23 | }; 24 | 25 | const initUserSettings = function () { 26 | var us = settings.userSettings; 27 | var fontSize = us.get('fontSize'); 28 | if (!fontSize) { 29 | us.set('fontSize', defaultSettings.fontSize); 30 | } 31 | }; 32 | 33 | const populateSettingsDialog = function () { 34 | var us = settings.userSettings; 35 | $('#font-size').val(us.get('fontSize')); 36 | $('#font-size').change(function () { 37 | var val = parseInt($('#font-size').val()); 38 | if (!val) val = defaultSettings.fontSize; 39 | editor.setFontSize(val); 40 | us.set('fontSize', val); 41 | $('#font-size').val(val); 42 | }); 43 | }; 44 | 45 | const run = function () { 46 | $('#output-container').html(''); 47 | $('#debugger').html(''); 48 | $('#restart').hide(); 49 | $('a[href="#tab-output"]').tab('show'); 50 | settings.compile({ 51 | showWarnings: function(msgs) { 52 | const WarningStyle = '"color: gold; background-color: gray"'; 53 | 54 | if ( msgs.length > 0 ) { 55 | $('#output').html('
' + msgs + '
'); 56 | } 57 | 58 | return; 59 | }, 60 | data: editor.getValue(), 61 | success: function (data, msgs) { 62 | $('#restart').show(); 63 | 64 | $('
', { id: 'output' }).appendTo('#output-container'); 65 | 66 | $('
').appendTo('#output-container'); 67 | 68 | this.showWarnings(msgs); 69 | // Show output 70 | if (data.indexOf('Failed') === 0) { 71 | $('#output').html(data); 72 | return; 73 | } 74 | 75 | try { 76 | eval(data); 77 | } 78 | catch (e) { 79 | $('#output').html(e); 80 | return; 81 | } 82 | 83 | $('#output').squiffy({ 84 | scroll: 'element', 85 | persist: false, 86 | restartPrompt: false, 87 | onSet: function (attribute, value) { 88 | onSet(attribute, value); 89 | } 90 | }); 91 | }, 92 | fail: function (data,msgs) { 93 | $('
', { id: 'output' }).appendTo('#output-container'); 94 | 95 | // Show fail message 96 | $('#output').html(data.message); 97 | 98 | // Show detailed info 99 | this.showWarnings(msgs); 100 | } 101 | }); 102 | }; 103 | 104 | const restart = function () { 105 | $('#debugger').html(''); 106 | $('#output').squiffy('restart'); 107 | }; 108 | 109 | const downloadSquiffyScript = function () { 110 | localSave(); 111 | download(editor.getValue(), title + '.squiffy'); 112 | }; 113 | 114 | const downloadZip = function () { 115 | localSave(); 116 | settings.compile({ 117 | data: editor.getValue(), 118 | success: function (data) { 119 | download(data, title + '.zip', 'application/octet-stream'); 120 | }, 121 | fail: function (data) { 122 | $('#output').html(data.message); 123 | }, 124 | zip: true 125 | }); 126 | }; 127 | 128 | const downloadJavascript = function () { 129 | localSave(); 130 | settings.compile({ 131 | data: editor.getValue(), 132 | success: function (data) { 133 | download(data, title + '.js'); 134 | }, 135 | fail: function (data) { 136 | $('#output').html(data.message); 137 | } 138 | }); 139 | }; 140 | 141 | const download = function (data, filename, type) { 142 | var blob = new Blob([data], {type: type || 'text/plain'}); 143 | var downloadLink = document.createElement('a'); 144 | downloadLink.download = filename; 145 | downloadLink.href = window.URL.createObjectURL(blob); 146 | downloadLink.onclick = function (e) { 147 | document.body.removeChild(e.target); 148 | }; 149 | downloadLink.style.display = 'none'; 150 | document.body.appendChild(downloadLink); 151 | downloadLink.click(); 152 | }; 153 | 154 | const addSection = function () { 155 | addSectionOrPassage(true); 156 | }; 157 | 158 | const addPassage = function () { 159 | addSectionOrPassage(false); 160 | }; 161 | 162 | const addSectionOrPassage = function (isSection) { 163 | const selection = editor.getSelectedText(); 164 | var text; 165 | 166 | if (isSection) { 167 | text = '[[' + selection + ']]'; 168 | } 169 | else { 170 | text = '[' + selection + ']'; 171 | } 172 | 173 | if (selection) { 174 | // replace the selected text with a link to new section/passage 175 | editor.session.replace(editor.selection.getRange(), text); 176 | } 177 | 178 | text = text + ':\n'; 179 | var insertLine = currentSection.end; 180 | var moveToLine = insertLine; 181 | if (!insertLine) { 182 | // adding new section/passage to the end of the document 183 | insertLine = editor.session.doc.$lines.length; 184 | text = '\n\n' + text; 185 | moveToLine = insertLine + 1; 186 | } 187 | else { 188 | // adding new section/passage in the middle of the document 189 | text = text + '\n\n'; 190 | } 191 | var Range = ace.require('ace/range').Range; 192 | var range = new Range(insertLine, 0, insertLine, 0); 193 | editor.session.replace(range, text); 194 | 195 | if (selection) { 196 | // move cursor to new section/passage 197 | moveTo(moveToLine + 1); 198 | } 199 | else { 200 | // no name was specified, so set cursor position to middle of [[ and ]] 201 | var column = isSection ? 2 : 1; 202 | moveTo(moveToLine, column); 203 | } 204 | }; 205 | 206 | const collapseAll = function () { 207 | editor.session.foldAll(); 208 | }; 209 | 210 | const uncollapseAll = function () { 211 | editor.session.unfold(); 212 | }; 213 | 214 | const showSettings = function () { 215 | $('#settings-dialog').modal(); 216 | }; 217 | 218 | var localSaveTimeout, autoSaveTimeout; 219 | 220 | const editorChange = function () { 221 | if (loading) return; 222 | setInfo(''); 223 | if (localSaveTimeout) clearTimeout(localSaveTimeout); 224 | localSaveTimeout = setTimeout(localSave, 50); 225 | if (settings.autoSave) { 226 | if (autoSaveTimeout) clearTimeout(autoSaveTimeout); 227 | autoSaveTimeout = setTimeout(autoSave, 5000); 228 | } 229 | if (settings.setDirty) settings.setDirty(); 230 | }; 231 | 232 | const localSave = function () { 233 | var data = editor.getValue(); 234 | if (settings.storageKey) { 235 | localStorage[settings.storageKey] = data; 236 | } 237 | processFile(data); 238 | }; 239 | 240 | const autoSave = function () { 241 | settings.autoSave(title); 242 | }; 243 | 244 | const setInfo = function (text) { 245 | $('#info').html(text); 246 | }; 247 | 248 | const processFile = function (data) { 249 | var titleRegex = /^@title (.*)$/; 250 | var sectionRegex = /^\[\[(.*)\]\]:$/; 251 | var passageRegex = /^\[(.*)\]:$/; 252 | var newTitle; 253 | 254 | sourceMap = [ 255 | { 256 | name: '(Default)', 257 | start: 0, 258 | isDefault: true, 259 | passages: [ 260 | { 261 | name: '(Default)', 262 | start: 0, 263 | } 264 | ] 265 | } 266 | ]; 267 | 268 | var lines = data.replace(/\r/g, '').split('\n'); 269 | 270 | const currentSection = function () { 271 | return sourceMap.slice(-1)[0]; 272 | }; 273 | 274 | const endPassage = function (index) { 275 | var previousPassage = currentSection().passages.slice(-1)[0]; 276 | if (!previousPassage) return; 277 | previousPassage.end = index; 278 | }; 279 | 280 | lines.forEach(function (line, index) { 281 | var stripLine = line.trim(); 282 | var titleMatch = titleRegex.exec(stripLine); 283 | var sectionMatch = sectionRegex.exec(stripLine); 284 | var passageMatch = passageRegex.exec(stripLine); 285 | 286 | if (titleMatch) { 287 | newTitle = titleMatch[1]; 288 | } 289 | 290 | if (sectionMatch) { 291 | endPassage(index); 292 | currentSection().end = index; 293 | var newSection = { 294 | name: sectionMatch[1], 295 | start: index, 296 | passages: [ 297 | { 298 | name: '(Default)', 299 | start: index 300 | } 301 | ] 302 | }; 303 | sourceMap.push(newSection); 304 | } 305 | else if (passageMatch) { 306 | endPassage(index); 307 | var newPassage = { 308 | name: passageMatch[1], 309 | start: index 310 | }; 311 | currentSection().passages.push(newPassage); 312 | } 313 | }); 314 | 315 | if (!title || title !== newTitle) { 316 | title = newTitle || 'Untitled'; 317 | if (settings.updateTitle) { 318 | settings.updateTitle(title); 319 | } 320 | } 321 | 322 | var selectSection = $('#sections'); 323 | selectSection.html(''); 324 | sourceMap.forEach(function (section) { 325 | var name = dropdownName(section.name); 326 | selectSection.append($('