diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..d71ac009f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,13 @@ +build/* linguist-vendored +csrc/core/* linguist-vendored +csrc/crypto/* linguist-vendored +csrc/http/* linguist-vendored +csrc/luapower/* linguist-vendored +csrc/luautf8/* linguist-vendored +csrc/unix/* linguist-vendored +csrc/dns/* linguist-vendored +examples/* linguist-vendored +jssrc/codemirror.js linguist-vendored +index.html linguist-vendored +readme.md linguist-vendored +*.css linguist-vendored diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..600acece3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,59 @@ +# Compiled Lua sources +luac.out + +# luarocks build files +*.src.rock +*.zip +*.tar.gz + +# Object files +*.o +*.os +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo +*.def +*.exp + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +lua_modules/ + +*~ +dist/ +build/ +node_modules/ + +experimental/build/continuation_templates.h +experimental/build/eve +experimental/build/path.c +experimental/build/Eve.xcodeproj/xcuserdata +experimental/build/Eve.xcodeproj/*/xcuserdata +experimental/build/lua +experimental/build/luajit-2.0 +experimental/build/*.js + +experimental/build/*.js +experimental/build/*.js.map diff --git a/.npmignore b/.npmignore new file mode 100644 index 000000000..e69de29bb diff --git a/ATTRIBUTIONS.md b/ATTRIBUTIONS.md new file mode 100644 index 000000000..895fcebb7 --- /dev/null +++ b/ATTRIBUTIONS.md @@ -0,0 +1,223 @@ +# Software Attributions + +Eve is built using the following technologies generously supplied by their attributed authors in accordance with the following licenses. If you recognize anything in this list as incorrect, please bring it to our attention and we will correct it. + +------------------------------------------------------------------------------- + +### @types/body-parser +- TypeScript definitions for body-parser +- By Santi Albo (https://github.com/santialbo/), VILIC VANE (https://vilic.info), Jonathan Häberle (https://github.com/dreampulse) +- [MIT][MIT] License +- https://www.npmjs.com/package/@types/body-parser + +------------------------------------------------------------------------------- + +### @types/commonmark +- TypeScript definitions for commonmark.js 0.22.1 +- By Nico Jansen (https://github.com/nicojs) +- [MIT][MIT] License +- https://www.npmjs.com/package/@types/commonmark + +------------------------------------------------------------------------------- + +### @types/express +- TypeScript definitions for Express 4.x +- By by Boris Yankov https://github.com/borisyankov/. +- [MIT][MIT] License +- https://www.npmjs.com/package/@types/express + +------------------------------------------------------------------------------- + +### @types/glob +- TypeScript definitions for Glob 5.0.10 +- By vvakame (https://github.com/vvakame) +- [MIT][MIT] License +- https://www.npmjs.com/package/@types/glob + +------------------------------------------------------------------------------- + +### @types/minimist +- TypeScript definitions for minimist 1.1.3 +- By Bart van der Schoor (https://github.com/Bartvds), Necroskillz (https://github.com/Necroskillz) +- [MIT][MIT] License +- https://www.npmjs.com/package/@types/minimist + +------------------------------------------------------------------------------- + +### @types/mkdirp +- TypeScript definitions for mkdirp 0.3.0 +- By Bart van der Schoor (https://github.com/Bartvds) +- [MIT][MIT] License +- https://www.npmjs.com/package/@types/mkdirp + +------------------------------------------------------------------------------- + +### @types/node +- TypeScript definitions for Node.js v6.x +- By Microsoft TypeScript (http://typescriptlang.org), DefinitelyTyped (https://github.com/DefinitelyTyped/DefinitelyTyped) +- [MIT][MIT] License +- https://www.npmjs.com/package/@types/node + +------------------------------------------------------------------------------- + +### @types/request +- TypeScript definitions for request +- By Carlos Ballesteros Velasco (https://github.com/soywiz), et. al. +- [MIT][MIT] License +- https://www.npmjs.com/package/@types/request + +------------------------------------------------------------------------------- + +### @types/tape +- TypeScript definitions for tape v4.2.2 +- By Bart van der Schoor (https://github.com/Bartvds), Haoqun Jiang (https://github.com/sodatea) +- [MIT][MIT] License +- https://www.npmjs.com/package/@types/tape + +------------------------------------------------------------------------------- + +### @types/ws +- TypeScript definitions for ws +- By Paul Loyd (https://github.com/loyd) +- [MIT][MIT] License +- https://www.npmjs.com/package/@types/ws + +------------------------------------------------------------------------------- + +### codemirror.d.ts +- TypeScript definitions for codemirror +- By mihailik (https://github.com/mihailik) +- [MIT][MIT] License +- [https://github.com/DefinitelyTyped/DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/03b3450d08a0b0a1b9e820e581a52c8e6c21f32e/codemirror/codemirror.d.ts) + +------------------------------------------------------------------------------- + +### System.js +- Universal dynamic module loader +- [MIT][MIT] License +- https://github.com/systemjs/systemjs + +------------------------------------------------------------------------------- + +### body-parser +- Node.js body parsing middleware +- [MIT][MIT] License +- https://www.npmjs.com/package/body-parser + +------------------------------------------------------------------------------- + +### chevrotain +- Chevrotain is a high performance fault tolerant javascript parsing DSL for building recursive decent parsers +- [Apache 2.0][Apache] License +- https://www.npmjs.com/package/chevrotain + +------------------------------------------------------------------------------- + +### commonmark +- a strongly specified, highly compatible variant of Markdown +- [BSD-2-Clause][BSD] License +- https://www.npmjs.com/package/commonmark + +------------------------------------------------------------------------------- + +### express +- Fast, unopinionated, minimalist web framework +- [MIT][MIT] License +- https://www.npmjs.com/package/express + +------------------------------------------------------------------------------- + +### glob +- a little globber +- by Isaac Schlueter (https://github.com/isaacs) +- [ISC][ISC] License +- https://www.npmjs.com/package/glob + +------------------------------------------------------------------------------- + +### minimist +- parse argument options +- [MIT][MIT] License +- https://www.npmjs.com/package/minimist + +------------------------------------------------------------------------------- + +### mkdirp +- Recursively mkdir, like mkdir -p +- [MIT][MIT] License +- https://www.npmjs.com/package/mkdirp + +------------------------------------------------------------------------------- + +### node-uuid +- Rigorous implementation of RFC4122 (v1 and v4) UUIDs. +- [MIT][MIT] License +- https://www.npmjs.com/package/node-uuid + +------------------------------------------------------------------------------- + +### request +- Simplified HTTP request client +- [Apache 2.0][Apache] License +- https://www.npmjs.com/package/request + +------------------------------------------------------------------------------- + +### typescript +- TypeScript is a language for application scale JavaScript development +- [Apache 2.0][Apache] License +- https://www.npmjs.com/package/typescript + +------------------------------------------------------------------------------- + +### ws +- simple to use, blazing fast and thoroughly tested websocket client, server and console for node.js, up-to-date against RFC-6455 +- [MIT][MIT] License +- https://www.npmjs.com/package/ws + +------------------------------------------------------------------------------- + +### uuid.js +- Simple, fast generation of RFC4122 UUIDS. +- By Robert Kieffer (https://github.com/broofa) +- [MIT][MIT] License +- https://github.com/broofa/node-uuid + +------------------------------------------------------------------------------- + +### Codemirror +- In-browser code editor +- By Marijn Haverbeke (https://github.com/marijnh) +- [MIT][MIT] License +- https://github.com/codemirror/codemirror + +------------------------------------------------------------------------------- + +### Simple Scrollbars +- Addon for CodeMirror +- By Marijn Haverbeke (https://github.com/marijnh) +- [MIT][MIT] License +- https://codemirror.net/doc/manual.html#addon_simplescrollbars + +------------------------------------------------------------------------------- + +### Annotate Scrollbar +- Addon for CodeMirror +- By Marijn Haverbeke (https://github.com/marijnh) +- [MIT][MIT] License +- https://codemirror.net/doc/manual.html#addon_annotatescrollbar + +------------------------------------------------------------------------------- + +### ionicons +The premium icon font for Ionic +- [MIT][MIT] License +- by Ben Sperry (https://twitter.com/benjsperry) +- https://github.com/driftyco/ionicons + +------------------------------------------------------------------------------- + +[BSD]: https://spdx.org/licenses/BSD-2-Clause +[MIT]: https://spdx.org/licenses/MIT +[Apache]: https://spdx.org/licenses/Apache-2.0 +[ISC]: https://spdx.org/licenses/ISC \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..1d2c0778d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM node:6-slim +MAINTAINER Kodowa, Inc. +ADD / /eve +RUN chown -R node:node /eve +USER node +ENV HOME /eve +WORKDIR /eve +RUN npm install +EXPOSE 8080 +CMD npm start \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..8dada3eda --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..f2c20e150 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: ./node_modules/.bin/tsc && cp src/*.js build/src/ && cp ./node_modules/chevrotain/lib/chevrotain.js build/src/ && node build/src/runtime/server.js diff --git a/README.md b/README.md new file mode 100644 index 000000000..bac599a7e --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +

+ Eve logo +

+ +--- + +Eve is a programming language and IDE based on years of research into building a human-first programming platform. You can play with Eve online here: [play.witheve.com](http://play.witheve.com/). + +[![Play With Eve](http://programming.witheve.com/images/eve.png)](http://play.witheve.com/) + +## Installation + +### From Source + +You'll need a recent [node.js](https://nodejs.org) for your platform. Download the Eve source either by cloning this repository: + + +``` +git clone https://github.com/witheve/Eve.git +``` + +or you can [download](https://github.com/witheve/Eve/archive/master.zip) the Eve source directly. To build and run Eve, run the following commands in the root Eve directory: + +``` +npm install +npm run build +npm start +``` + +Then open `http://localhost:8080/` in your browser. + +### From npm + +Alternatively, you can download Eve directly from npm with the following command: + +``` +npm install witheve +``` + +### From Docker + +First, [download](https://www.docker.com/products/docker) and install Docker for your platform. To download and install the Eve container, run the following command: + +``` +docker run -p 8080:8080 witheve/eve +``` + +## How to use Eve + +You can learn about Eve with the following resources: + +- [Play with Eve in your browser](http://play.witheve.com/) (use Chrome for best results) +- [Syntax Quick Reference](https://witheve.github.io/assets/docs/SyntaxReference.pdf) +- [Eve Language Handbook (draft)](http://docs.witheve.com) + +*Please let us know what kind of documents would be the most helpful as you begin your journey with Eve*. We want our documentation to be a highlight of the Eve experience, so any suggestions are greatly appreciated. + +### Running Eve Programs in Server Mode + +By default, Eve executes on the client browser. To instead execute your program on the server, launch Eve with the `--server` flag: + +``` +npm start -- --server +``` + +## Get Involved + +### Join the Community + +The Eve community is small but constantly growing, and everyone is welcome! + +- Join or start a discussion on our [mailing list](https://groups.google.com/forum/#!forum/eve-talk). +- Impact the future of Eve by getting involved with our [Request for Comments](https://github.com/witheve/rfcs) process. +- Read our [development blog](http://incidentalcomplexity.com/). +- Follow us on [Twitter](https://twitter.com/with_eve). + +### How to Contribute + +The best way to contribute right now is to write Eve code and report your experiences. Let us know what kind of programs you’re trying to write, what barriers you are facing in writing code (both mental and technological), and any errors you encounter along the way. Also, let us know what you love! What features are your favorite? + +Another way to really help us is to host your `*.eve` files on Github, so we can get Eve recognized as an official language in the eyes of Github. Be sure to also send us a link to your repo! + +### How to File an Issue + +Please file any issues in this repository. Before you file an issue, please take a look to see if the issue already exists. When you file an issue, please include: + +1. The steps needed to reproduce the bug +2. Your operating system and browser. +3. If applicable, the .*eve file that causes the bug. + +## License + +Eve is licensed under the Apache 2.0 license, see [LICENSE](https://github.com/witheve/eve/blob/master/LICENSE) for details. + +## Disclaimer + +Eve is currently at a very early, "alpha" stage of development. This means the language, tools, and docs are largely incomplete, but undergoing rapid and continuous development. If you encounter errors while using Eve, don't worry: it's likely our fault. Please bring the problem to our attention by [filing an issue](https://github.com/witheve/eve#how-to-file-an-issue). + +As always, with pre-release software, don’t use this for anything important. We are continuously pushing to this codebase, so you can expect very rapid changes. At this time, we’re not prepared make the commitment that our changes will not break your code, but we’ll do our best to [update you](https://groups.google.com/forum/#!forum/eve-talk) on the biggest changes. diff --git a/bin/eve.js b/bin/eve.js new file mode 100755 index 000000000..38ad58fa7 --- /dev/null +++ b/bin/eve.js @@ -0,0 +1,61 @@ +#!/usr/bin/env node + +var path = require("path"); +var fs = require("fs"); +var minimist = require("minimist"); + +var config = require("../build/src/config"); +var Owner = config.Owner; +var server = require("../build/src/runtime/server"); + +const argv = minimist(process.argv.slice(2), {boolean: ["server", "editor"]}); + +// Since our current development pattern uses npm as its package repository, we treat the nearest ancestor directory with a package.json (inclusive) as the directory's "root". +function findRoot(root) { + var pkg; + root = root.split(path.sep); + while(!pkg && root.length > 1) { + var cur = root.join(path.sep); + if(fs.existsSync(path.join(cur, "package.json"))) { + return cur; + } + root.pop(); + } +} + + +var port = argv["port"] || process.env.PORT || 8080; +var runtimeOwner = argv["server"] ? Owner.server : Owner.client; +var controlOwner = argv["localControl"] ? Owner.client : Owner.server; +var editor = argv["editor"] || false; +var filepath = argv["_"][0]; +var internal = false; + +var root = findRoot(process.cwd()); +var eveRoot = findRoot(__dirname); + + +// If we're executing within the eve module/repo, we're running internally and should expose our examples, src, etc. +// This should be handled down the road by some sort of a manifest in conjunction with the `root` rather than hardcoding. +if(root === eveRoot) internal = true; +else if(!root) { + internal = true; + // We shouldn't (and when globally installed, *can't*) taint the internal examples when running as an installed binary. + // @TODO: In the future we should have a more flexible solution that can copy out the examples into your current workspace when edited. + controlOwner = Owner.client; +} + + +// If we're not given an explicit filepath to run, assume the user wanted the editor (rather than a blank page). +// Similarly, if we're running internally, send the user over to the quickstart, since they're likely testing the waters. +if(!filepath) { + editor = true; + if(internal) filepath = eveRoot + "/" + "examples/quickstart.eve"; +} else { + filepath = path.resolve(filepath); +} + +var opts = {internal: internal, runtimeOwner: runtimeOwner, controlOwner: controlOwner, editor: editor, port: port, path: filepath, internal: internal, root: root, eveRoot: eveRoot}; +config.init(opts); + +server.run(opts); diff --git a/css/ide.css b/css/ide.css new file mode 100644 index 000000000..354cf50a0 --- /dev/null +++ b/css/ide.css @@ -0,0 +1,290 @@ +.flex-spacer { flex: 1; } +.flex-row { display: flex; flex: 1; flex-direction: row; } + +@keyframes hide-delayed { + from { opacity: 1; } + 99% { opacity: 1; } + to { opacity: 0; } +} + +* { box-sizing: border-box; } + + +html, body, __root { height: 100%; } +body { background: #f5f5f5; color: #555; } +button { font-family: "Avenir", "Helvetica neue", sans-serif; } +/****************************************************************************\ + * Root +\****************************************************************************/ +body { justify-content: stretch; } +.application-root { overflow: auto; } + +.editor-root { display: flex; flex-direction: row; height: 100%; align-items: stretch; background: #fff; } + +.navigator-pane { width: 280px; flex: 0 0 auto; align-self: stretch; overflow-x: hidden; background:#555; color:#eee; transition: width 0.3s ease-in-out, background-color 0.2s ease-in-out; padding-bottom:30px; } +.navigator-pane.collapsed { width: 20px; background-color: #fff; } +.navigator-pane.collapsed:hover { background: #555; color: #eee; } +.navigator-pane .navigator-pane-inner { width: 280px; padding: 0 5px; margin-left: 0; transition: margin-left 0.3s ease-in-out; } +.navigator-pane.collapsed .navigator-pane-inner { margin-left: -255px; } +.navigator-pane.collapsed .tree-item { pointer-events: none; } + +.editor-pane { position: relative; height: 100%; } + +.CodeMirror-sizer > div:first-child { width: 650px; min-height: 100%; } + +/****************************************************************************\ + * Navigator +\****************************************************************************/ +.navigator-header { display: flex; flex-direction: column; padding:10px 5px; user-select: none; -webkit-user-select: none; margin-bottom:10px; font-size:18px; color: #555; transition:color 0.2s ease-in-out; } +.navigator-pane:hover .navigator-header { color: #aaa; } +.navigator-header > .controls { display:flex; flex-direction:row; flex:1; } +.navigator-header > .controls > * { height: 20px; text-align: center; transition: transform 0.3s; cursor: pointer; } +.navigator-header > .controls > div:hover { color: #eee; } +.navigator-pane.collapsed:hover .navigator-header > .controls > div { color: #eee; } +.navigator-header:hover > .controls > *:not(.disabled) { } +.navigator-header .collapse-btn { transform: rotate(180deg); color: #999; } +.navigator-header .up-btn .label { display: flex; font-size:16px; margin-left: 10px; } +.navigator-header .label { white-space: pre; display:none; } + +.navigator-header .inspector-controls { margin-top: 20px; } +.navigator-header .inspector-controls button {border:none; color:#eee; outline:none; cursor:pointer; padding:3px 10px; font-size: 10pt; margin-left:20px; background:#666; border-radius:2px; margin-top: 20px; transition:background 0.1s ease-in-out; } +.navigator-header .inspector-controls button:hover { background: #777; } + + + +.tree-item { flex-direction: column; align-items: stretch; } +.tree-item > .tree-items { display: flex; flex-direction: column; } + +.item-level-1 > div > .label { font-size:14pt; } +.item-level-1 > .flex-row { margin-bottom:25px; } +.tree-item + .item-level-1 > .flex-row { margin-top: 25px; } +.item-level-2 { margin-bottom:25px; margin-left:3px; } +.item-level-2 > div > .label { font-weight: bold; margin-bottom:5px; } +.item-level-3 { margin-top:3px; } +.item-level-3 > div > .label { color:#bbb; font-size: 11pt; } + +.tree-item .label { display: flex; flex: 1 1 auto; margin-left: 20px; user-select: none; -webkit-user-select: none; transition: opacity 0.15s; cursor: pointer;} +.tree-item .label:before { display: inline-block; margin-left: -15px; margin-right: 5px; height: 15px; width: 15px; text-align: center; } +.tree-item.branch .label:before { opacity: 0.25; transition: transform 0.15s; } +.tree-item.branch.root > .flex-row > .label { opacity: 0; pointer-events: none; } +.tree-item.branch.collapsed .label:before { transform: rotate(-90deg); opacity: 0.5; } +.tree-item.branch:hover .label:before { opacity: 1; } +.tree-item .label.no-icon:before { content: ""; } + +.tree-item .controls { display: flex; flex: 0 0 auto; align-items: center; color:#bbb; margin-left: 5px; opacity: 0; transition: opacity 0.15s; user-select: none; -webkit-user-select: none; } +.tree-item .controls > * { cursor:pointer; display: inline-block; width: 20px; text-align: center; } +.tree-item .controls > div:hover { color: #eee; } +.tree-item:hover > .flex-row > .controls { opacity: 1; } + +.tree-item.hidden > .flex-row > .label { opacity: 0.5; } + +/****************************************************************************\ + * Notices +\****************************************************************************/ +.main-pane { display: flex; flex-direction: column; justify-content: stretch; overflow-y: hidden; } +.main-pane .notices { display: flex; flex: 0 0 auto; } + +.main-pane .notices > .notice { padding: 5px 60px; background: #e0e0e0; } +.main-pane .notices > .notice + .notice { border-bottom: 1px solid #d0d0d0; } +.main-pane .notices > .notice > .time { margin-right: 10px; } + +.main-pane .notices > .notice.error { background: rgb(255, 216, 222); } +.main-pane .notices > .notice.warning { background: rgb(255, 247, 217); } + +/****************************************************************************\ + * Editor +\****************************************************************************/ +.editor-pane { margin-left: 50px; } + +.editor-pane > .controls { position: absolute; top: 10px; right: 30px; z-index: 10; background: rgba(255, 255, 255, 1); } +.editor-pane > .controls > div { padding: 5px 10px; font-size:22px; color: #999; border-radius:2px; } +.editor-pane > .controls > div:hover { background: #eee; color: #555; } + +.CodeMirror-lines { padding-top:30px; } + +.CodeMirror { background:none; color: inherit; height:100%; font-size:11pt; } +.CodeMirror-scroll, .CodeMirror-gutters { height:100%; } +.CodeMirror-cursor { background: #555; border-color:#555; } + +.CodeMirror { color: #555; line-height:25px; font-family: Avenir, "Helvetica Neue", sans-serif; } +.CodeMirror pre { padding-left: 8px; padding-right:50px; } +.CodeMirror-scroll { scroll-behavior: smooth; } +.CodeMirror-gutters { background: #303030; border-right: none; padding-right: 5px; } +.CodeMirror-selected { background: rgba(0,117,255,0.1); } +.CodeMirror-focused .CodeMirror-selected { background: rgba(0,117,255,0.2); } + +.CodeMirror-simplescroll-vertical { width:14px; background: #fff; border-left:1px solid #ddd; border-right:1px solid #ddd; } +.CodeMirror-simplescroll-vertical div { border:none; border-radius:0; background: #f4f4f4; border-top:1px solid #ddd; border-bottom:1px solid #ddd; } +.CodeMirror .scrollbar-annotation { width: 8px !important; min-height:3px !important; height:3px !important; right: 3px !important; } + +.CodeMirror .STRONG { font-weight:bold; } +.CodeMirror .EMPH { font-style:italic; } +.CodeMirror .CODE { margin-left: 10px; margin-right:50px; border-left:5px solid #eaeaea; background: #f4f4f4; font-family: "Inconsolata", "Monaco", "Consolas", "Ubuntu Mono", monospace; } +.CodeMirror .CODE-TEXT { line-height:19px; padding-left:30px; transition: opacity 0.3s; } +.CodeMirror .CODE-TEXT.CODE-DISABLED { opacity: 0.6; } +.CodeMirror span.CODE { color: #0076ce; background:none; margin:0; padding:0; border:none; } + +.CodeMirror .CODE_BLOCK { font-family: "Inconsolata", "Monaco", "Consolas", "Ubuntu Mono", monospace; } +.CodeMirror .CODE-TOP { border-radius:3px 3px 0 0; padding-top:8px; } +.CodeMirror .CODE-BOTTOM { border-radius:0 0 3px 3px; padding-bottom:8px; } + +.CodeMirror .CodeMirror-linewidget { overflow: visible; z-index: 3; } +.CodeMirror .code-controls-widget { display: flex; flex-direction: row; justify-content: flex-end; margin-right: 50px; padding: 5px 10px; padding-bottom: 0; height: 15px; font-size: 1.2rem; opacity: 0.2; transition: opacity 0.3s; } +.CodeMirror .code-controls-widget > * { cursor:pointer; } +.CodeMirror .CodeMirror-linewidget:hover .code-controls-widget { opacity: 1; } + +.CodeMirror .code-footer-widget { height: 15px; } + +.CodeMirror .DOC { color: #d8d8d8; } +.CodeMirror .HEADING1 { font-size:30px; padding-top:20px; padding-bottom: 30px;} +.CodeMirror .HEADING2 { font-size:20px; padding-top:20px; padding-bottom: 10px; font-weight: bold; } +.CodeMirror .HEADING3 { font-size:16px; padding-top:20px; padding-bottom: 10px; font-weight: bold; } +.CodeMirror .HEADING4 { font-size:12pt;} + +.CodeMirror .link { color: #0079d3; } +.CodeMirror .link-widget { margin-right: 8px; } + +/* Syntax Styles */ + +.CodeMirror .COMMENT { color: #747474; } + +/* Variables/Attributes */ +.CodeMirror .ALIAS { } +.CodeMirror .TAG, .CodeMirror .TAG + .IDENTIFIER { color: #50edf6; color: #4bcbd3; color: #0076ce; } +.CodeMirror .NAME, .CodeMirror .NAME + .IDENTIFIER { color: #50edf6; color: #4bcbd3; color: #0076ce; } +/* Phases */ +.CodeMirror .ACTION, .SEARCH { color: black; } +/* Statements */ +.CodeMirror .IF, .ELSE, .THEN { color: black; } +/* Literals */ +.CodeMirror .STRING, .QUOTE, .BOOL, .NUM { color: #00a588; } +/* Brackets */ +.CodeMirror .OPEN-BRACKET, .CodeMirror .CLOSE-BRACKET, .CodeMirror .OPEN-PAREN, .CodeMirror .CLOSE-PAREN { color: gray; } +.CodeMirror .STRING-EMBED-OPEN, .CodeMirror .STRING-EMBED-CLOSE { color: gray; } +.CodeMirror .DOT, .CodeMirror .COMMA { color: gray; } +/* Binding/Action Operators */ +.CodeMirror .EQUALITY, .CodeMirror .MERGE, .CodeMirror .SET, .CodeMirror .MUTATE { color: gray; } +/* Infix Operators */ +.CodeMirror .INFIX, .CodeMirror .COMPARISON { color: gray; } + +/****************************************************************************\ + * Elision +\****************************************************************************/ + +.elision { border-top:1px solid #ddd; } + +/****************************************************************************\ + * Format Bars +\****************************************************************************/ +.editor-pane .new-block-bar { position: absolute; top: 0; left: 0; margin-left: -28px; display: flex; flex-direction: row; width: 28px; overflow: hidden; transition: width 0.15s; z-index: 5; color: #666; border-radius:5px; border:1px solid #ddd; font-size:10pt; background:white; } +.editor-pane .new-block-bar.active { width: 246px; } + + +.editor-pane .new-block-bar > .new-block-bar-toggle, +.editor-pane .new-block-bar > .controls > div { display:flex; padding: 3px 10px; align-items:center; cursor:pointer; } + +.editor-pane .new-block-bar > .new-block-bar-toggle { padding: 3px 8px; } + +.editor-pane > .controls > div { cursor:pointer; } +.editor-pane .new-block-bar > .new-block-bar-toggle:hover, +.editor-pane .new-block-bar > .controls > div:hover{ background: #e4e4e4; } +.editor-pane .new-block-bar > .new-block-bar-toggle:before { transition: transform 0.15s; } +.editor-pane .new-block-bar.active > .new-block-bar-toggle:before { transform: rotate(45deg); } + +.editor-pane .format-bar { position: absolute; top: 0; left: 0; display: flex; flex-direction: row; background: #666; color: #eee; z-index: 9; border-radius:2px; overflow:hidden; } +.editor-pane .format-bar > * { padding: 5px 10px; cursor:pointer; } +.editor-pane .format-bar > div:hover { background: #888; color: #fff; } + + +/****************************************************************************\ + * Comments +\****************************************************************************/ + +.comment-widget { padding: 2px 0; } +.code-comment-widget { padding-left: 30px; margin-top: 2px; margin-left: 10px; margin-right: 50px; border-left: 5px solid #ff7e92; } + +.comment-widget.error { background: rgb(255, 216, 222); color: #b7003e; } + +.comment-widget .comment { font-size: 10pt; } + +.comment-widget .comment-inner { display: block; } +.comment-inner { display: none; } + +.comment-widget .quick-actions { display: flex; flex-direction: row; flex-wrap: wrap; opacity: 0.5; transition: opacity 0.15s; } +.comment-widget:hover .quick-actions { opacity: 1; } +.comment-widget .quick-actions > .comment-action { flex: 1 0 auto; max-width: 50%; padding: 2px 5px; margin-top: 5px; text-align: center; background: #404040; } +.comment-widget .quick-actions > .comment-action + .comment-action { margin-left: 1px; } +.comment-widget .quick-actions > .comment-action:hover { background: #505050; } + +.comment.error { border-color: #F06060; color: #b7003e; } +.comment.warning { border-color: #C0C060; } + +.CodeMirror .document_comment { } +.CodeMirror .document_comment.error { border-bottom: 1px solid #ff7e92; } +.CodeMirror .document_comment.warning { border-bottom: 1px solid #C0C060; } + +.CodeMirror .scrollbar-annotation { background: #AAA; min-height: 5px; opacity: 1; transition: opacity 0.15s; z-index: 2; } +.CodeMirror .scrollbar-annotation:hover { opacity: 0.5; } +.CodeMirror .scrollbar-annotation.error { background: #ff7e92; } +.CodeMirror .scrollbar-annotation.warning { background: #C0C060; } +.CodeMirror .scrollbar-annotation.affector, .CodeMirror .scrollbar-annotation.source { background: #66b1e9; } + +/* .CodeMirror .COMMENT_error { border-color: #ff7e92; } */ + +/****************************************************************************\ + * Views +\****************************************************************************/ +.program > .view { display: none !important; } + +.view-container { margin:10px 50px 0px 10px; padding-left: 20px; border-left:5px solid #96e4d7; background: #d1f5eb; } +.CodeMirror-linewidget + .CodeMirror-linewidget .view-container { margin-top:0; } + +table.view { border-collapse: collapse; } +table.view thead { border: 1px solid #ccc; } +table.view td { padding: 0 5px; border: 1px solid #ccc; border-top: none; border-bottom: none; } + +.view.kv-table { display: table; border-collapse: collapse; border: 1px solid #202020; } +.view.kv-table .kv-row { display: table-row; } +.view.kv-table .kv-row + .kv-row { border-top: 1px solid #606060; } +.view.kv-table .kv-row > div { display: table-cell; padding: 0 5px; } +.view.kv-table .kv-values { border-left: 1px solid #202020; } + +.view.bar-graph { position: relative; display: flex; flex-direction: row; align-items: flex-end; } +.view.bar-graph .bar-graph-bar { display: flex; align-items: flex-end; margin:10px 0; justify-content: center; min-height: 1px; background: #6ed2c1; margin-right: 2px; transition: 0.3s width, 0.3s height; } + +/****************************************************************************\ + * Inspector +\****************************************************************************/ +.editor-pane .controls .inspector-button.waiting { color: #ff0028; background: #ffdde2; } +.editor-pane .controls .inspector-button.inspecting { color: #ff0028; background: #ffdde2; } + +.CodeMirror .code.annotated { border-left-color: #66b1e9; } +.CodeMirror .code.annotated.annotated_performance-red { border-left-color: #c65555; } +.CodeMirror .code.annotated.annotated_performance-orange { border-left-color: #e9c070; } +.CodeMirror .scrollbar-annotation.performance-red { background: #c65555; min-height: 5px; opacity: 1; transition: opacity 0.15s; z-index: 2; } +.CodeMirror .scrollbar-annotation.performance-orange { background: #e9c070; min-height: 5px; opacity: 1; transition: opacity 0.15s; z-index: 2; } + +.code-comment-widget.performance-green { white-space: pre; background: #e0e0e0; border-left: 5px solid #EEEEAA; } +.code-comment-widget.performance-orange { white-space: pre; background: #ffe9c0; border-left: 5px solid #e9c070; } +.code-comment-widget.performance-red { white-space: pre; background: #f9cccc; border-left: 5px solid #c65555; } + +.CodeMirror .shadow { color: #999 !important; } +.CodeMirror .highlight { background: rgba(127, 192, 255, 0.4); } +.CodeMirror .badge { } +.CodeMirror .badge-widget { display: inline-block; margin-left: 2px; font-size: 10pt; color: #E91E63; padding: 0 3px; } +.CodeMirror .badge-widget:before { content: "( "; } +.CodeMirror .badge-widget:after { content: " )"; } +.CodeMirror .cause-of-failure { background: rgb(255, 200, 215); } + +.inspector-pane { overflow:hidden; display:flex; width:250px; background: #666; flex-direction: column; color: #eee; border-radius:2px; box-shadow: 0 2px 8px #aaa; z-index: 10; } +.inspector-pane .buttons { display:flex; flex-direction:column; } +.inspector-pane .buttons button { background:none; border:none; color: #eee; font-size: 10pt; width: 100%; padding: 8px 15px; text-align:left; } +.inspector-pane .buttons button + button { border-top:1px solid #555; } + +.inspector-pane .kv-table { border:none; width:100%; background: #777; border-radius: 2px 2px 0 0; } +.inspector-pane .kv-key { color: #ddd; } +.inspector-pane .kv-table .kv-values { border-left: 1px solid #555; } +.inspector-pane .kv-table .kv-row > div { padding: 4px 15px; font-size:11pt; } +.inspector-pane .kv-table .kv-row { border:none; border-bottom:1px solid #555; } + +.inspector-pane div[is-entity="true"] { color: #A6D0FF; } diff --git a/css/ionicons.min.css b/css/ionicons.min.css new file mode 100644 index 000000000..841dec15e --- /dev/null +++ b/css/ionicons.min.css @@ -0,0 +1,11 @@ +@charset "UTF-8";/*! + Ionicons, v2.0.1 + Created by Ben Sperry for the Ionic Framework, http://ionicons.com/ + https://twitter.com/benjsperry https://twitter.com/ionicframework + MIT License: https://github.com/driftyco/ionicons + + Android-style icons originally built by Google’s + Material Design Icons: https://github.com/google/material-design-icons + used under CC BY http://creativecommons.org/licenses/by/4.0/ + Modified icons to fit ionicon’s grid from original. +*/@font-face{font-family:"Ionicons";src:url("../fonts/ionicons.eot?v=2.0.1");src:url("../fonts/ionicons.eot?v=2.0.1#iefix") format("embedded-opentype"),url("../fonts/ionicons.ttf?v=2.0.1") format("truetype"),url("../fonts/ionicons.woff?v=2.0.1") format("woff"),url("../fonts/ionicons.svg?v=2.0.1#Ionicons") format("svg");font-weight:normal;font-style:normal}.ion,.ionicons,.ion-alert:before,.ion-alert-circled:before,.ion-android-add:before,.ion-android-add-circle:before,.ion-android-alarm-clock:before,.ion-android-alert:before,.ion-android-apps:before,.ion-android-archive:before,.ion-android-arrow-back:before,.ion-android-arrow-down:before,.ion-android-arrow-dropdown:before,.ion-android-arrow-dropdown-circle:before,.ion-android-arrow-dropleft:before,.ion-android-arrow-dropleft-circle:before,.ion-android-arrow-dropright:before,.ion-android-arrow-dropright-circle:before,.ion-android-arrow-dropup:before,.ion-android-arrow-dropup-circle:before,.ion-android-arrow-forward:before,.ion-android-arrow-up:before,.ion-android-attach:before,.ion-android-bar:before,.ion-android-bicycle:before,.ion-android-boat:before,.ion-android-bookmark:before,.ion-android-bulb:before,.ion-android-bus:before,.ion-android-calendar:before,.ion-android-call:before,.ion-android-camera:before,.ion-android-cancel:before,.ion-android-car:before,.ion-android-cart:before,.ion-android-chat:before,.ion-android-checkbox:before,.ion-android-checkbox-blank:before,.ion-android-checkbox-outline:before,.ion-android-checkbox-outline-blank:before,.ion-android-checkmark-circle:before,.ion-android-clipboard:before,.ion-android-close:before,.ion-android-cloud:before,.ion-android-cloud-circle:before,.ion-android-cloud-done:before,.ion-android-cloud-outline:before,.ion-android-color-palette:before,.ion-android-compass:before,.ion-android-contact:before,.ion-android-contacts:before,.ion-android-contract:before,.ion-android-create:before,.ion-android-delete:before,.ion-android-desktop:before,.ion-android-document:before,.ion-android-done:before,.ion-android-done-all:before,.ion-android-download:before,.ion-android-drafts:before,.ion-android-exit:before,.ion-android-expand:before,.ion-android-favorite:before,.ion-android-favorite-outline:before,.ion-android-film:before,.ion-android-folder:before,.ion-android-folder-open:before,.ion-android-funnel:before,.ion-android-globe:before,.ion-android-hand:before,.ion-android-hangout:before,.ion-android-happy:before,.ion-android-home:before,.ion-android-image:before,.ion-android-laptop:before,.ion-android-list:before,.ion-android-locate:before,.ion-android-lock:before,.ion-android-mail:before,.ion-android-map:before,.ion-android-menu:before,.ion-android-microphone:before,.ion-android-microphone-off:before,.ion-android-more-horizontal:before,.ion-android-more-vertical:before,.ion-android-navigate:before,.ion-android-notifications:before,.ion-android-notifications-none:before,.ion-android-notifications-off:before,.ion-android-open:before,.ion-android-options:before,.ion-android-people:before,.ion-android-person:before,.ion-android-person-add:before,.ion-android-phone-landscape:before,.ion-android-phone-portrait:before,.ion-android-pin:before,.ion-android-plane:before,.ion-android-playstore:before,.ion-android-print:before,.ion-android-radio-button-off:before,.ion-android-radio-button-on:before,.ion-android-refresh:before,.ion-android-remove:before,.ion-android-remove-circle:before,.ion-android-restaurant:before,.ion-android-sad:before,.ion-android-search:before,.ion-android-send:before,.ion-android-settings:before,.ion-android-share:before,.ion-android-share-alt:before,.ion-android-star:before,.ion-android-star-half:before,.ion-android-star-outline:before,.ion-android-stopwatch:before,.ion-android-subway:before,.ion-android-sunny:before,.ion-android-sync:before,.ion-android-textsms:before,.ion-android-time:before,.ion-android-train:before,.ion-android-unlock:before,.ion-android-upload:before,.ion-android-volume-down:before,.ion-android-volume-mute:before,.ion-android-volume-off:before,.ion-android-volume-up:before,.ion-android-walk:before,.ion-android-warning:before,.ion-android-watch:before,.ion-android-wifi:before,.ion-aperture:before,.ion-archive:before,.ion-arrow-down-a:before,.ion-arrow-down-b:before,.ion-arrow-down-c:before,.ion-arrow-expand:before,.ion-arrow-graph-down-left:before,.ion-arrow-graph-down-right:before,.ion-arrow-graph-up-left:before,.ion-arrow-graph-up-right:before,.ion-arrow-left-a:before,.ion-arrow-left-b:before,.ion-arrow-left-c:before,.ion-arrow-move:before,.ion-arrow-resize:before,.ion-arrow-return-left:before,.ion-arrow-return-right:before,.ion-arrow-right-a:before,.ion-arrow-right-b:before,.ion-arrow-right-c:before,.ion-arrow-shrink:before,.ion-arrow-swap:before,.ion-arrow-up-a:before,.ion-arrow-up-b:before,.ion-arrow-up-c:before,.ion-asterisk:before,.ion-at:before,.ion-backspace:before,.ion-backspace-outline:before,.ion-bag:before,.ion-battery-charging:before,.ion-battery-empty:before,.ion-battery-full:before,.ion-battery-half:before,.ion-battery-low:before,.ion-beaker:before,.ion-beer:before,.ion-bluetooth:before,.ion-bonfire:before,.ion-bookmark:before,.ion-bowtie:before,.ion-briefcase:before,.ion-bug:before,.ion-calculator:before,.ion-calendar:before,.ion-camera:before,.ion-card:before,.ion-cash:before,.ion-chatbox:before,.ion-chatbox-working:before,.ion-chatboxes:before,.ion-chatbubble:before,.ion-chatbubble-working:before,.ion-chatbubbles:before,.ion-checkmark:before,.ion-checkmark-circled:before,.ion-checkmark-round:before,.ion-chevron-down:before,.ion-chevron-left:before,.ion-chevron-right:before,.ion-chevron-up:before,.ion-clipboard:before,.ion-clock:before,.ion-close:before,.ion-close-circled:before,.ion-close-round:before,.ion-closed-captioning:before,.ion-cloud:before,.ion-code:before,.ion-code-download:before,.ion-code-working:before,.ion-coffee:before,.ion-compass:before,.ion-compose:before,.ion-connection-bars:before,.ion-contrast:before,.ion-crop:before,.ion-cube:before,.ion-disc:before,.ion-document:before,.ion-document-text:before,.ion-drag:before,.ion-earth:before,.ion-easel:before,.ion-edit:before,.ion-egg:before,.ion-eject:before,.ion-email:before,.ion-email-unread:before,.ion-erlenmeyer-flask:before,.ion-erlenmeyer-flask-bubbles:before,.ion-eye:before,.ion-eye-disabled:before,.ion-female:before,.ion-filing:before,.ion-film-marker:before,.ion-fireball:before,.ion-flag:before,.ion-flame:before,.ion-flash:before,.ion-flash-off:before,.ion-folder:before,.ion-fork:before,.ion-fork-repo:before,.ion-forward:before,.ion-funnel:before,.ion-gear-a:before,.ion-gear-b:before,.ion-grid:before,.ion-hammer:before,.ion-happy:before,.ion-happy-outline:before,.ion-headphone:before,.ion-heart:before,.ion-heart-broken:before,.ion-help:before,.ion-help-buoy:before,.ion-help-circled:before,.ion-home:before,.ion-icecream:before,.ion-image:before,.ion-images:before,.ion-information:before,.ion-information-circled:before,.ion-ionic:before,.ion-ios-alarm:before,.ion-ios-alarm-outline:before,.ion-ios-albums:before,.ion-ios-albums-outline:before,.ion-ios-americanfootball:before,.ion-ios-americanfootball-outline:before,.ion-ios-analytics:before,.ion-ios-analytics-outline:before,.ion-ios-arrow-back:before,.ion-ios-arrow-down:before,.ion-ios-arrow-forward:before,.ion-ios-arrow-left:before,.ion-ios-arrow-right:before,.ion-ios-arrow-thin-down:before,.ion-ios-arrow-thin-left:before,.ion-ios-arrow-thin-right:before,.ion-ios-arrow-thin-up:before,.ion-ios-arrow-up:before,.ion-ios-at:before,.ion-ios-at-outline:before,.ion-ios-barcode:before,.ion-ios-barcode-outline:before,.ion-ios-baseball:before,.ion-ios-baseball-outline:before,.ion-ios-basketball:before,.ion-ios-basketball-outline:before,.ion-ios-bell:before,.ion-ios-bell-outline:before,.ion-ios-body:before,.ion-ios-body-outline:before,.ion-ios-bolt:before,.ion-ios-bolt-outline:before,.ion-ios-book:before,.ion-ios-book-outline:before,.ion-ios-bookmarks:before,.ion-ios-bookmarks-outline:before,.ion-ios-box:before,.ion-ios-box-outline:before,.ion-ios-briefcase:before,.ion-ios-briefcase-outline:before,.ion-ios-browsers:before,.ion-ios-browsers-outline:before,.ion-ios-calculator:before,.ion-ios-calculator-outline:before,.ion-ios-calendar:before,.ion-ios-calendar-outline:before,.ion-ios-camera:before,.ion-ios-camera-outline:before,.ion-ios-cart:before,.ion-ios-cart-outline:before,.ion-ios-chatboxes:before,.ion-ios-chatboxes-outline:before,.ion-ios-chatbubble:before,.ion-ios-chatbubble-outline:before,.ion-ios-checkmark:before,.ion-ios-checkmark-empty:before,.ion-ios-checkmark-outline:before,.ion-ios-circle-filled:before,.ion-ios-circle-outline:before,.ion-ios-clock:before,.ion-ios-clock-outline:before,.ion-ios-close:before,.ion-ios-close-empty:before,.ion-ios-close-outline:before,.ion-ios-cloud:before,.ion-ios-cloud-download:before,.ion-ios-cloud-download-outline:before,.ion-ios-cloud-outline:before,.ion-ios-cloud-upload:before,.ion-ios-cloud-upload-outline:before,.ion-ios-cloudy:before,.ion-ios-cloudy-night:before,.ion-ios-cloudy-night-outline:before,.ion-ios-cloudy-outline:before,.ion-ios-cog:before,.ion-ios-cog-outline:before,.ion-ios-color-filter:before,.ion-ios-color-filter-outline:before,.ion-ios-color-wand:before,.ion-ios-color-wand-outline:before,.ion-ios-compose:before,.ion-ios-compose-outline:before,.ion-ios-contact:before,.ion-ios-contact-outline:before,.ion-ios-copy:before,.ion-ios-copy-outline:before,.ion-ios-crop:before,.ion-ios-crop-strong:before,.ion-ios-download:before,.ion-ios-download-outline:before,.ion-ios-drag:before,.ion-ios-email:before,.ion-ios-email-outline:before,.ion-ios-eye:before,.ion-ios-eye-outline:before,.ion-ios-fastforward:before,.ion-ios-fastforward-outline:before,.ion-ios-filing:before,.ion-ios-filing-outline:before,.ion-ios-film:before,.ion-ios-film-outline:before,.ion-ios-flag:before,.ion-ios-flag-outline:before,.ion-ios-flame:before,.ion-ios-flame-outline:before,.ion-ios-flask:before,.ion-ios-flask-outline:before,.ion-ios-flower:before,.ion-ios-flower-outline:before,.ion-ios-folder:before,.ion-ios-folder-outline:before,.ion-ios-football:before,.ion-ios-football-outline:before,.ion-ios-game-controller-a:before,.ion-ios-game-controller-a-outline:before,.ion-ios-game-controller-b:before,.ion-ios-game-controller-b-outline:before,.ion-ios-gear:before,.ion-ios-gear-outline:before,.ion-ios-glasses:before,.ion-ios-glasses-outline:before,.ion-ios-grid-view:before,.ion-ios-grid-view-outline:before,.ion-ios-heart:before,.ion-ios-heart-outline:before,.ion-ios-help:before,.ion-ios-help-empty:before,.ion-ios-help-outline:before,.ion-ios-home:before,.ion-ios-home-outline:before,.ion-ios-infinite:before,.ion-ios-infinite-outline:before,.ion-ios-information:before,.ion-ios-information-empty:before,.ion-ios-information-outline:before,.ion-ios-ionic-outline:before,.ion-ios-keypad:before,.ion-ios-keypad-outline:before,.ion-ios-lightbulb:before,.ion-ios-lightbulb-outline:before,.ion-ios-list:before,.ion-ios-list-outline:before,.ion-ios-location:before,.ion-ios-location-outline:before,.ion-ios-locked:before,.ion-ios-locked-outline:before,.ion-ios-loop:before,.ion-ios-loop-strong:before,.ion-ios-medical:before,.ion-ios-medical-outline:before,.ion-ios-medkit:before,.ion-ios-medkit-outline:before,.ion-ios-mic:before,.ion-ios-mic-off:before,.ion-ios-mic-outline:before,.ion-ios-minus:before,.ion-ios-minus-empty:before,.ion-ios-minus-outline:before,.ion-ios-monitor:before,.ion-ios-monitor-outline:before,.ion-ios-moon:before,.ion-ios-moon-outline:before,.ion-ios-more:before,.ion-ios-more-outline:before,.ion-ios-musical-note:before,.ion-ios-musical-notes:before,.ion-ios-navigate:before,.ion-ios-navigate-outline:before,.ion-ios-nutrition:before,.ion-ios-nutrition-outline:before,.ion-ios-paper:before,.ion-ios-paper-outline:before,.ion-ios-paperplane:before,.ion-ios-paperplane-outline:before,.ion-ios-partlysunny:before,.ion-ios-partlysunny-outline:before,.ion-ios-pause:before,.ion-ios-pause-outline:before,.ion-ios-paw:before,.ion-ios-paw-outline:before,.ion-ios-people:before,.ion-ios-people-outline:before,.ion-ios-person:before,.ion-ios-person-outline:before,.ion-ios-personadd:before,.ion-ios-personadd-outline:before,.ion-ios-photos:before,.ion-ios-photos-outline:before,.ion-ios-pie:before,.ion-ios-pie-outline:before,.ion-ios-pint:before,.ion-ios-pint-outline:before,.ion-ios-play:before,.ion-ios-play-outline:before,.ion-ios-plus:before,.ion-ios-plus-empty:before,.ion-ios-plus-outline:before,.ion-ios-pricetag:before,.ion-ios-pricetag-outline:before,.ion-ios-pricetags:before,.ion-ios-pricetags-outline:before,.ion-ios-printer:before,.ion-ios-printer-outline:before,.ion-ios-pulse:before,.ion-ios-pulse-strong:before,.ion-ios-rainy:before,.ion-ios-rainy-outline:before,.ion-ios-recording:before,.ion-ios-recording-outline:before,.ion-ios-redo:before,.ion-ios-redo-outline:before,.ion-ios-refresh:before,.ion-ios-refresh-empty:before,.ion-ios-refresh-outline:before,.ion-ios-reload:before,.ion-ios-reverse-camera:before,.ion-ios-reverse-camera-outline:before,.ion-ios-rewind:before,.ion-ios-rewind-outline:before,.ion-ios-rose:before,.ion-ios-rose-outline:before,.ion-ios-search:before,.ion-ios-search-strong:before,.ion-ios-settings:before,.ion-ios-settings-strong:before,.ion-ios-shuffle:before,.ion-ios-shuffle-strong:before,.ion-ios-skipbackward:before,.ion-ios-skipbackward-outline:before,.ion-ios-skipforward:before,.ion-ios-skipforward-outline:before,.ion-ios-snowy:before,.ion-ios-speedometer:before,.ion-ios-speedometer-outline:before,.ion-ios-star:before,.ion-ios-star-half:before,.ion-ios-star-outline:before,.ion-ios-stopwatch:before,.ion-ios-stopwatch-outline:before,.ion-ios-sunny:before,.ion-ios-sunny-outline:before,.ion-ios-telephone:before,.ion-ios-telephone-outline:before,.ion-ios-tennisball:before,.ion-ios-tennisball-outline:before,.ion-ios-thunderstorm:before,.ion-ios-thunderstorm-outline:before,.ion-ios-time:before,.ion-ios-time-outline:before,.ion-ios-timer:before,.ion-ios-timer-outline:before,.ion-ios-toggle:before,.ion-ios-toggle-outline:before,.ion-ios-trash:before,.ion-ios-trash-outline:before,.ion-ios-undo:before,.ion-ios-undo-outline:before,.ion-ios-unlocked:before,.ion-ios-unlocked-outline:before,.ion-ios-upload:before,.ion-ios-upload-outline:before,.ion-ios-videocam:before,.ion-ios-videocam-outline:before,.ion-ios-volume-high:before,.ion-ios-volume-low:before,.ion-ios-wineglass:before,.ion-ios-wineglass-outline:before,.ion-ios-world:before,.ion-ios-world-outline:before,.ion-ipad:before,.ion-iphone:before,.ion-ipod:before,.ion-jet:before,.ion-key:before,.ion-knife:before,.ion-laptop:before,.ion-leaf:before,.ion-levels:before,.ion-lightbulb:before,.ion-link:before,.ion-load-a:before,.ion-load-b:before,.ion-load-c:before,.ion-load-d:before,.ion-location:before,.ion-lock-combination:before,.ion-locked:before,.ion-log-in:before,.ion-log-out:before,.ion-loop:before,.ion-magnet:before,.ion-male:before,.ion-man:before,.ion-map:before,.ion-medkit:before,.ion-merge:before,.ion-mic-a:before,.ion-mic-b:before,.ion-mic-c:before,.ion-minus:before,.ion-minus-circled:before,.ion-minus-round:before,.ion-model-s:before,.ion-monitor:before,.ion-more:before,.ion-mouse:before,.ion-music-note:before,.ion-navicon:before,.ion-navicon-round:before,.ion-navigate:before,.ion-network:before,.ion-no-smoking:before,.ion-nuclear:before,.ion-outlet:before,.ion-paintbrush:before,.ion-paintbucket:before,.ion-paper-airplane:before,.ion-paperclip:before,.ion-pause:before,.ion-person:before,.ion-person-add:before,.ion-person-stalker:before,.ion-pie-graph:before,.ion-pin:before,.ion-pinpoint:before,.ion-pizza:before,.ion-plane:before,.ion-planet:before,.ion-play:before,.ion-playstation:before,.ion-plus:before,.ion-plus-circled:before,.ion-plus-round:before,.ion-podium:before,.ion-pound:before,.ion-power:before,.ion-pricetag:before,.ion-pricetags:before,.ion-printer:before,.ion-pull-request:before,.ion-qr-scanner:before,.ion-quote:before,.ion-radio-waves:before,.ion-record:before,.ion-refresh:before,.ion-reply:before,.ion-reply-all:before,.ion-ribbon-a:before,.ion-ribbon-b:before,.ion-sad:before,.ion-sad-outline:before,.ion-scissors:before,.ion-search:before,.ion-settings:before,.ion-share:before,.ion-shuffle:before,.ion-skip-backward:before,.ion-skip-forward:before,.ion-social-android:before,.ion-social-android-outline:before,.ion-social-angular:before,.ion-social-angular-outline:before,.ion-social-apple:before,.ion-social-apple-outline:before,.ion-social-bitcoin:before,.ion-social-bitcoin-outline:before,.ion-social-buffer:before,.ion-social-buffer-outline:before,.ion-social-chrome:before,.ion-social-chrome-outline:before,.ion-social-codepen:before,.ion-social-codepen-outline:before,.ion-social-css3:before,.ion-social-css3-outline:before,.ion-social-designernews:before,.ion-social-designernews-outline:before,.ion-social-dribbble:before,.ion-social-dribbble-outline:before,.ion-social-dropbox:before,.ion-social-dropbox-outline:before,.ion-social-euro:before,.ion-social-euro-outline:before,.ion-social-facebook:before,.ion-social-facebook-outline:before,.ion-social-foursquare:before,.ion-social-foursquare-outline:before,.ion-social-freebsd-devil:before,.ion-social-github:before,.ion-social-github-outline:before,.ion-social-google:before,.ion-social-google-outline:before,.ion-social-googleplus:before,.ion-social-googleplus-outline:before,.ion-social-hackernews:before,.ion-social-hackernews-outline:before,.ion-social-html5:before,.ion-social-html5-outline:before,.ion-social-instagram:before,.ion-social-instagram-outline:before,.ion-social-javascript:before,.ion-social-javascript-outline:before,.ion-social-linkedin:before,.ion-social-linkedin-outline:before,.ion-social-markdown:before,.ion-social-nodejs:before,.ion-social-octocat:before,.ion-social-pinterest:before,.ion-social-pinterest-outline:before,.ion-social-python:before,.ion-social-reddit:before,.ion-social-reddit-outline:before,.ion-social-rss:before,.ion-social-rss-outline:before,.ion-social-sass:before,.ion-social-skype:before,.ion-social-skype-outline:before,.ion-social-snapchat:before,.ion-social-snapchat-outline:before,.ion-social-tumblr:before,.ion-social-tumblr-outline:before,.ion-social-tux:before,.ion-social-twitch:before,.ion-social-twitch-outline:before,.ion-social-twitter:before,.ion-social-twitter-outline:before,.ion-social-usd:before,.ion-social-usd-outline:before,.ion-social-vimeo:before,.ion-social-vimeo-outline:before,.ion-social-whatsapp:before,.ion-social-whatsapp-outline:before,.ion-social-windows:before,.ion-social-windows-outline:before,.ion-social-wordpress:before,.ion-social-wordpress-outline:before,.ion-social-yahoo:before,.ion-social-yahoo-outline:before,.ion-social-yen:before,.ion-social-yen-outline:before,.ion-social-youtube:before,.ion-social-youtube-outline:before,.ion-soup-can:before,.ion-soup-can-outline:before,.ion-speakerphone:before,.ion-speedometer:before,.ion-spoon:before,.ion-star:before,.ion-stats-bars:before,.ion-steam:before,.ion-stop:before,.ion-thermometer:before,.ion-thumbsdown:before,.ion-thumbsup:before,.ion-toggle:before,.ion-toggle-filled:before,.ion-transgender:before,.ion-trash-a:before,.ion-trash-b:before,.ion-trophy:before,.ion-tshirt:before,.ion-tshirt-outline:before,.ion-umbrella:before,.ion-university:before,.ion-unlocked:before,.ion-upload:before,.ion-usb:before,.ion-videocamera:before,.ion-volume-high:before,.ion-volume-low:before,.ion-volume-medium:before,.ion-volume-mute:before,.ion-wand:before,.ion-waterdrop:before,.ion-wifi:before,.ion-wineglass:before,.ion-woman:before,.ion-wrench:before,.ion-xbox:before{display:inline-block;font-family:"Ionicons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;text-rendering:auto;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.ion-alert:before{content:"\f101"}.ion-alert-circled:before{content:"\f100"}.ion-android-add:before{content:"\f2c7"}.ion-android-add-circle:before{content:"\f359"}.ion-android-alarm-clock:before{content:"\f35a"}.ion-android-alert:before{content:"\f35b"}.ion-android-apps:before{content:"\f35c"}.ion-android-archive:before{content:"\f2c9"}.ion-android-arrow-back:before{content:"\f2ca"}.ion-android-arrow-down:before{content:"\f35d"}.ion-android-arrow-dropdown:before{content:"\f35f"}.ion-android-arrow-dropdown-circle:before{content:"\f35e"}.ion-android-arrow-dropleft:before{content:"\f361"}.ion-android-arrow-dropleft-circle:before{content:"\f360"}.ion-android-arrow-dropright:before{content:"\f363"}.ion-android-arrow-dropright-circle:before{content:"\f362"}.ion-android-arrow-dropup:before{content:"\f365"}.ion-android-arrow-dropup-circle:before{content:"\f364"}.ion-android-arrow-forward:before{content:"\f30f"}.ion-android-arrow-up:before{content:"\f366"}.ion-android-attach:before{content:"\f367"}.ion-android-bar:before{content:"\f368"}.ion-android-bicycle:before{content:"\f369"}.ion-android-boat:before{content:"\f36a"}.ion-android-bookmark:before{content:"\f36b"}.ion-android-bulb:before{content:"\f36c"}.ion-android-bus:before{content:"\f36d"}.ion-android-calendar:before{content:"\f2d1"}.ion-android-call:before{content:"\f2d2"}.ion-android-camera:before{content:"\f2d3"}.ion-android-cancel:before{content:"\f36e"}.ion-android-car:before{content:"\f36f"}.ion-android-cart:before{content:"\f370"}.ion-android-chat:before{content:"\f2d4"}.ion-android-checkbox:before{content:"\f374"}.ion-android-checkbox-blank:before{content:"\f371"}.ion-android-checkbox-outline:before{content:"\f373"}.ion-android-checkbox-outline-blank:before{content:"\f372"}.ion-android-checkmark-circle:before{content:"\f375"}.ion-android-clipboard:before{content:"\f376"}.ion-android-close:before{content:"\f2d7"}.ion-android-cloud:before{content:"\f37a"}.ion-android-cloud-circle:before{content:"\f377"}.ion-android-cloud-done:before{content:"\f378"}.ion-android-cloud-outline:before{content:"\f379"}.ion-android-color-palette:before{content:"\f37b"}.ion-android-compass:before{content:"\f37c"}.ion-android-contact:before{content:"\f2d8"}.ion-android-contacts:before{content:"\f2d9"}.ion-android-contract:before{content:"\f37d"}.ion-android-create:before{content:"\f37e"}.ion-android-delete:before{content:"\f37f"}.ion-android-desktop:before{content:"\f380"}.ion-android-document:before{content:"\f381"}.ion-android-done:before{content:"\f383"}.ion-android-done-all:before{content:"\f382"}.ion-android-download:before{content:"\f2dd"}.ion-android-drafts:before{content:"\f384"}.ion-android-exit:before{content:"\f385"}.ion-android-expand:before{content:"\f386"}.ion-android-favorite:before{content:"\f388"}.ion-android-favorite-outline:before{content:"\f387"}.ion-android-film:before{content:"\f389"}.ion-android-folder:before{content:"\f2e0"}.ion-android-folder-open:before{content:"\f38a"}.ion-android-funnel:before{content:"\f38b"}.ion-android-globe:before{content:"\f38c"}.ion-android-hand:before{content:"\f2e3"}.ion-android-hangout:before{content:"\f38d"}.ion-android-happy:before{content:"\f38e"}.ion-android-home:before{content:"\f38f"}.ion-android-image:before{content:"\f2e4"}.ion-android-laptop:before{content:"\f390"}.ion-android-list:before{content:"\f391"}.ion-android-locate:before{content:"\f2e9"}.ion-android-lock:before{content:"\f392"}.ion-android-mail:before{content:"\f2eb"}.ion-android-map:before{content:"\f393"}.ion-android-menu:before{content:"\f394"}.ion-android-microphone:before{content:"\f2ec"}.ion-android-microphone-off:before{content:"\f395"}.ion-android-more-horizontal:before{content:"\f396"}.ion-android-more-vertical:before{content:"\f397"}.ion-android-navigate:before{content:"\f398"}.ion-android-notifications:before{content:"\f39b"}.ion-android-notifications-none:before{content:"\f399"}.ion-android-notifications-off:before{content:"\f39a"}.ion-android-open:before{content:"\f39c"}.ion-android-options:before{content:"\f39d"}.ion-android-people:before{content:"\f39e"}.ion-android-person:before{content:"\f3a0"}.ion-android-person-add:before{content:"\f39f"}.ion-android-phone-landscape:before{content:"\f3a1"}.ion-android-phone-portrait:before{content:"\f3a2"}.ion-android-pin:before{content:"\f3a3"}.ion-android-plane:before{content:"\f3a4"}.ion-android-playstore:before{content:"\f2f0"}.ion-android-print:before{content:"\f3a5"}.ion-android-radio-button-off:before{content:"\f3a6"}.ion-android-radio-button-on:before{content:"\f3a7"}.ion-android-refresh:before{content:"\f3a8"}.ion-android-remove:before{content:"\f2f4"}.ion-android-remove-circle:before{content:"\f3a9"}.ion-android-restaurant:before{content:"\f3aa"}.ion-android-sad:before{content:"\f3ab"}.ion-android-search:before{content:"\f2f5"}.ion-android-send:before{content:"\f2f6"}.ion-android-settings:before{content:"\f2f7"}.ion-android-share:before{content:"\f2f8"}.ion-android-share-alt:before{content:"\f3ac"}.ion-android-star:before{content:"\f2fc"}.ion-android-star-half:before{content:"\f3ad"}.ion-android-star-outline:before{content:"\f3ae"}.ion-android-stopwatch:before{content:"\f2fd"}.ion-android-subway:before{content:"\f3af"}.ion-android-sunny:before{content:"\f3b0"}.ion-android-sync:before{content:"\f3b1"}.ion-android-textsms:before{content:"\f3b2"}.ion-android-time:before{content:"\f3b3"}.ion-android-train:before{content:"\f3b4"}.ion-android-unlock:before{content:"\f3b5"}.ion-android-upload:before{content:"\f3b6"}.ion-android-volume-down:before{content:"\f3b7"}.ion-android-volume-mute:before{content:"\f3b8"}.ion-android-volume-off:before{content:"\f3b9"}.ion-android-volume-up:before{content:"\f3ba"}.ion-android-walk:before{content:"\f3bb"}.ion-android-warning:before{content:"\f3bc"}.ion-android-watch:before{content:"\f3bd"}.ion-android-wifi:before{content:"\f305"}.ion-aperture:before{content:"\f313"}.ion-archive:before{content:"\f102"}.ion-arrow-down-a:before{content:"\f103"}.ion-arrow-down-b:before{content:"\f104"}.ion-arrow-down-c:before{content:"\f105"}.ion-arrow-expand:before{content:"\f25e"}.ion-arrow-graph-down-left:before{content:"\f25f"}.ion-arrow-graph-down-right:before{content:"\f260"}.ion-arrow-graph-up-left:before{content:"\f261"}.ion-arrow-graph-up-right:before{content:"\f262"}.ion-arrow-left-a:before{content:"\f106"}.ion-arrow-left-b:before{content:"\f107"}.ion-arrow-left-c:before{content:"\f108"}.ion-arrow-move:before{content:"\f263"}.ion-arrow-resize:before{content:"\f264"}.ion-arrow-return-left:before{content:"\f265"}.ion-arrow-return-right:before{content:"\f266"}.ion-arrow-right-a:before{content:"\f109"}.ion-arrow-right-b:before{content:"\f10a"}.ion-arrow-right-c:before{content:"\f10b"}.ion-arrow-shrink:before{content:"\f267"}.ion-arrow-swap:before{content:"\f268"}.ion-arrow-up-a:before{content:"\f10c"}.ion-arrow-up-b:before{content:"\f10d"}.ion-arrow-up-c:before{content:"\f10e"}.ion-asterisk:before{content:"\f314"}.ion-at:before{content:"\f10f"}.ion-backspace:before{content:"\f3bf"}.ion-backspace-outline:before{content:"\f3be"}.ion-bag:before{content:"\f110"}.ion-battery-charging:before{content:"\f111"}.ion-battery-empty:before{content:"\f112"}.ion-battery-full:before{content:"\f113"}.ion-battery-half:before{content:"\f114"}.ion-battery-low:before{content:"\f115"}.ion-beaker:before{content:"\f269"}.ion-beer:before{content:"\f26a"}.ion-bluetooth:before{content:"\f116"}.ion-bonfire:before{content:"\f315"}.ion-bookmark:before{content:"\f26b"}.ion-bowtie:before{content:"\f3c0"}.ion-briefcase:before{content:"\f26c"}.ion-bug:before{content:"\f2be"}.ion-calculator:before{content:"\f26d"}.ion-calendar:before{content:"\f117"}.ion-camera:before{content:"\f118"}.ion-card:before{content:"\f119"}.ion-cash:before{content:"\f316"}.ion-chatbox:before{content:"\f11b"}.ion-chatbox-working:before{content:"\f11a"}.ion-chatboxes:before{content:"\f11c"}.ion-chatbubble:before{content:"\f11e"}.ion-chatbubble-working:before{content:"\f11d"}.ion-chatbubbles:before{content:"\f11f"}.ion-checkmark:before{content:"\f122"}.ion-checkmark-circled:before{content:"\f120"}.ion-checkmark-round:before{content:"\f121"}.ion-chevron-down:before{content:"\f123"}.ion-chevron-left:before{content:"\f124"}.ion-chevron-right:before{content:"\f125"}.ion-chevron-up:before{content:"\f126"}.ion-clipboard:before{content:"\f127"}.ion-clock:before{content:"\f26e"}.ion-close:before{content:"\f12a"}.ion-close-circled:before{content:"\f128"}.ion-close-round:before{content:"\f129"}.ion-closed-captioning:before{content:"\f317"}.ion-cloud:before{content:"\f12b"}.ion-code:before{content:"\f271"}.ion-code-download:before{content:"\f26f"}.ion-code-working:before{content:"\f270"}.ion-coffee:before{content:"\f272"}.ion-compass:before{content:"\f273"}.ion-compose:before{content:"\f12c"}.ion-connection-bars:before{content:"\f274"}.ion-contrast:before{content:"\f275"}.ion-crop:before{content:"\f3c1"}.ion-cube:before{content:"\f318"}.ion-disc:before{content:"\f12d"}.ion-document:before{content:"\f12f"}.ion-document-text:before{content:"\f12e"}.ion-drag:before{content:"\f130"}.ion-earth:before{content:"\f276"}.ion-easel:before{content:"\f3c2"}.ion-edit:before{content:"\f2bf"}.ion-egg:before{content:"\f277"}.ion-eject:before{content:"\f131"}.ion-email:before{content:"\f132"}.ion-email-unread:before{content:"\f3c3"}.ion-erlenmeyer-flask:before{content:"\f3c5"}.ion-erlenmeyer-flask-bubbles:before{content:"\f3c4"}.ion-eye:before{content:"\f133"}.ion-eye-disabled:before{content:"\f306"}.ion-female:before{content:"\f278"}.ion-filing:before{content:"\f134"}.ion-film-marker:before{content:"\f135"}.ion-fireball:before{content:"\f319"}.ion-flag:before{content:"\f279"}.ion-flame:before{content:"\f31a"}.ion-flash:before{content:"\f137"}.ion-flash-off:before{content:"\f136"}.ion-folder:before{content:"\f139"}.ion-fork:before{content:"\f27a"}.ion-fork-repo:before{content:"\f2c0"}.ion-forward:before{content:"\f13a"}.ion-funnel:before{content:"\f31b"}.ion-gear-a:before{content:"\f13d"}.ion-gear-b:before{content:"\f13e"}.ion-grid:before{content:"\f13f"}.ion-hammer:before{content:"\f27b"}.ion-happy:before{content:"\f31c"}.ion-happy-outline:before{content:"\f3c6"}.ion-headphone:before{content:"\f140"}.ion-heart:before{content:"\f141"}.ion-heart-broken:before{content:"\f31d"}.ion-help:before{content:"\f143"}.ion-help-buoy:before{content:"\f27c"}.ion-help-circled:before{content:"\f142"}.ion-home:before{content:"\f144"}.ion-icecream:before{content:"\f27d"}.ion-image:before{content:"\f147"}.ion-images:before{content:"\f148"}.ion-information:before{content:"\f14a"}.ion-information-circled:before{content:"\f149"}.ion-ionic:before{content:"\f14b"}.ion-ios-alarm:before{content:"\f3c8"}.ion-ios-alarm-outline:before{content:"\f3c7"}.ion-ios-albums:before{content:"\f3ca"}.ion-ios-albums-outline:before{content:"\f3c9"}.ion-ios-americanfootball:before{content:"\f3cc"}.ion-ios-americanfootball-outline:before{content:"\f3cb"}.ion-ios-analytics:before{content:"\f3ce"}.ion-ios-analytics-outline:before{content:"\f3cd"}.ion-ios-arrow-back:before{content:"\f3cf"}.ion-ios-arrow-down:before{content:"\f3d0"}.ion-ios-arrow-forward:before{content:"\f3d1"}.ion-ios-arrow-left:before{content:"\f3d2"}.ion-ios-arrow-right:before{content:"\f3d3"}.ion-ios-arrow-thin-down:before{content:"\f3d4"}.ion-ios-arrow-thin-left:before{content:"\f3d5"}.ion-ios-arrow-thin-right:before{content:"\f3d6"}.ion-ios-arrow-thin-up:before{content:"\f3d7"}.ion-ios-arrow-up:before{content:"\f3d8"}.ion-ios-at:before{content:"\f3da"}.ion-ios-at-outline:before{content:"\f3d9"}.ion-ios-barcode:before{content:"\f3dc"}.ion-ios-barcode-outline:before{content:"\f3db"}.ion-ios-baseball:before{content:"\f3de"}.ion-ios-baseball-outline:before{content:"\f3dd"}.ion-ios-basketball:before{content:"\f3e0"}.ion-ios-basketball-outline:before{content:"\f3df"}.ion-ios-bell:before{content:"\f3e2"}.ion-ios-bell-outline:before{content:"\f3e1"}.ion-ios-body:before{content:"\f3e4"}.ion-ios-body-outline:before{content:"\f3e3"}.ion-ios-bolt:before{content:"\f3e6"}.ion-ios-bolt-outline:before{content:"\f3e5"}.ion-ios-book:before{content:"\f3e8"}.ion-ios-book-outline:before{content:"\f3e7"}.ion-ios-bookmarks:before{content:"\f3ea"}.ion-ios-bookmarks-outline:before{content:"\f3e9"}.ion-ios-box:before{content:"\f3ec"}.ion-ios-box-outline:before{content:"\f3eb"}.ion-ios-briefcase:before{content:"\f3ee"}.ion-ios-briefcase-outline:before{content:"\f3ed"}.ion-ios-browsers:before{content:"\f3f0"}.ion-ios-browsers-outline:before{content:"\f3ef"}.ion-ios-calculator:before{content:"\f3f2"}.ion-ios-calculator-outline:before{content:"\f3f1"}.ion-ios-calendar:before{content:"\f3f4"}.ion-ios-calendar-outline:before{content:"\f3f3"}.ion-ios-camera:before{content:"\f3f6"}.ion-ios-camera-outline:before{content:"\f3f5"}.ion-ios-cart:before{content:"\f3f8"}.ion-ios-cart-outline:before{content:"\f3f7"}.ion-ios-chatboxes:before{content:"\f3fa"}.ion-ios-chatboxes-outline:before{content:"\f3f9"}.ion-ios-chatbubble:before{content:"\f3fc"}.ion-ios-chatbubble-outline:before{content:"\f3fb"}.ion-ios-checkmark:before{content:"\f3ff"}.ion-ios-checkmark-empty:before{content:"\f3fd"}.ion-ios-checkmark-outline:before{content:"\f3fe"}.ion-ios-circle-filled:before{content:"\f400"}.ion-ios-circle-outline:before{content:"\f401"}.ion-ios-clock:before{content:"\f403"}.ion-ios-clock-outline:before{content:"\f402"}.ion-ios-close:before{content:"\f406"}.ion-ios-close-empty:before{content:"\f404"}.ion-ios-close-outline:before{content:"\f405"}.ion-ios-cloud:before{content:"\f40c"}.ion-ios-cloud-download:before{content:"\f408"}.ion-ios-cloud-download-outline:before{content:"\f407"}.ion-ios-cloud-outline:before{content:"\f409"}.ion-ios-cloud-upload:before{content:"\f40b"}.ion-ios-cloud-upload-outline:before{content:"\f40a"}.ion-ios-cloudy:before{content:"\f410"}.ion-ios-cloudy-night:before{content:"\f40e"}.ion-ios-cloudy-night-outline:before{content:"\f40d"}.ion-ios-cloudy-outline:before{content:"\f40f"}.ion-ios-cog:before{content:"\f412"}.ion-ios-cog-outline:before{content:"\f411"}.ion-ios-color-filter:before{content:"\f414"}.ion-ios-color-filter-outline:before{content:"\f413"}.ion-ios-color-wand:before{content:"\f416"}.ion-ios-color-wand-outline:before{content:"\f415"}.ion-ios-compose:before{content:"\f418"}.ion-ios-compose-outline:before{content:"\f417"}.ion-ios-contact:before{content:"\f41a"}.ion-ios-contact-outline:before{content:"\f419"}.ion-ios-copy:before{content:"\f41c"}.ion-ios-copy-outline:before{content:"\f41b"}.ion-ios-crop:before{content:"\f41e"}.ion-ios-crop-strong:before{content:"\f41d"}.ion-ios-download:before{content:"\f420"}.ion-ios-download-outline:before{content:"\f41f"}.ion-ios-drag:before{content:"\f421"}.ion-ios-email:before{content:"\f423"}.ion-ios-email-outline:before{content:"\f422"}.ion-ios-eye:before{content:"\f425"}.ion-ios-eye-outline:before{content:"\f424"}.ion-ios-fastforward:before{content:"\f427"}.ion-ios-fastforward-outline:before{content:"\f426"}.ion-ios-filing:before{content:"\f429"}.ion-ios-filing-outline:before{content:"\f428"}.ion-ios-film:before{content:"\f42b"}.ion-ios-film-outline:before{content:"\f42a"}.ion-ios-flag:before{content:"\f42d"}.ion-ios-flag-outline:before{content:"\f42c"}.ion-ios-flame:before{content:"\f42f"}.ion-ios-flame-outline:before{content:"\f42e"}.ion-ios-flask:before{content:"\f431"}.ion-ios-flask-outline:before{content:"\f430"}.ion-ios-flower:before{content:"\f433"}.ion-ios-flower-outline:before{content:"\f432"}.ion-ios-folder:before{content:"\f435"}.ion-ios-folder-outline:before{content:"\f434"}.ion-ios-football:before{content:"\f437"}.ion-ios-football-outline:before{content:"\f436"}.ion-ios-game-controller-a:before{content:"\f439"}.ion-ios-game-controller-a-outline:before{content:"\f438"}.ion-ios-game-controller-b:before{content:"\f43b"}.ion-ios-game-controller-b-outline:before{content:"\f43a"}.ion-ios-gear:before{content:"\f43d"}.ion-ios-gear-outline:before{content:"\f43c"}.ion-ios-glasses:before{content:"\f43f"}.ion-ios-glasses-outline:before{content:"\f43e"}.ion-ios-grid-view:before{content:"\f441"}.ion-ios-grid-view-outline:before{content:"\f440"}.ion-ios-heart:before{content:"\f443"}.ion-ios-heart-outline:before{content:"\f442"}.ion-ios-help:before{content:"\f446"}.ion-ios-help-empty:before{content:"\f444"}.ion-ios-help-outline:before{content:"\f445"}.ion-ios-home:before{content:"\f448"}.ion-ios-home-outline:before{content:"\f447"}.ion-ios-infinite:before{content:"\f44a"}.ion-ios-infinite-outline:before{content:"\f449"}.ion-ios-information:before{content:"\f44d"}.ion-ios-information-empty:before{content:"\f44b"}.ion-ios-information-outline:before{content:"\f44c"}.ion-ios-ionic-outline:before{content:"\f44e"}.ion-ios-keypad:before{content:"\f450"}.ion-ios-keypad-outline:before{content:"\f44f"}.ion-ios-lightbulb:before{content:"\f452"}.ion-ios-lightbulb-outline:before{content:"\f451"}.ion-ios-list:before{content:"\f454"}.ion-ios-list-outline:before{content:"\f453"}.ion-ios-location:before{content:"\f456"}.ion-ios-location-outline:before{content:"\f455"}.ion-ios-locked:before{content:"\f458"}.ion-ios-locked-outline:before{content:"\f457"}.ion-ios-loop:before{content:"\f45a"}.ion-ios-loop-strong:before{content:"\f459"}.ion-ios-medical:before{content:"\f45c"}.ion-ios-medical-outline:before{content:"\f45b"}.ion-ios-medkit:before{content:"\f45e"}.ion-ios-medkit-outline:before{content:"\f45d"}.ion-ios-mic:before{content:"\f461"}.ion-ios-mic-off:before{content:"\f45f"}.ion-ios-mic-outline:before{content:"\f460"}.ion-ios-minus:before{content:"\f464"}.ion-ios-minus-empty:before{content:"\f462"}.ion-ios-minus-outline:before{content:"\f463"}.ion-ios-monitor:before{content:"\f466"}.ion-ios-monitor-outline:before{content:"\f465"}.ion-ios-moon:before{content:"\f468"}.ion-ios-moon-outline:before{content:"\f467"}.ion-ios-more:before{content:"\f46a"}.ion-ios-more-outline:before{content:"\f469"}.ion-ios-musical-note:before{content:"\f46b"}.ion-ios-musical-notes:before{content:"\f46c"}.ion-ios-navigate:before{content:"\f46e"}.ion-ios-navigate-outline:before{content:"\f46d"}.ion-ios-nutrition:before{content:"\f470"}.ion-ios-nutrition-outline:before{content:"\f46f"}.ion-ios-paper:before{content:"\f472"}.ion-ios-paper-outline:before{content:"\f471"}.ion-ios-paperplane:before{content:"\f474"}.ion-ios-paperplane-outline:before{content:"\f473"}.ion-ios-partlysunny:before{content:"\f476"}.ion-ios-partlysunny-outline:before{content:"\f475"}.ion-ios-pause:before{content:"\f478"}.ion-ios-pause-outline:before{content:"\f477"}.ion-ios-paw:before{content:"\f47a"}.ion-ios-paw-outline:before{content:"\f479"}.ion-ios-people:before{content:"\f47c"}.ion-ios-people-outline:before{content:"\f47b"}.ion-ios-person:before{content:"\f47e"}.ion-ios-person-outline:before{content:"\f47d"}.ion-ios-personadd:before{content:"\f480"}.ion-ios-personadd-outline:before{content:"\f47f"}.ion-ios-photos:before{content:"\f482"}.ion-ios-photos-outline:before{content:"\f481"}.ion-ios-pie:before{content:"\f484"}.ion-ios-pie-outline:before{content:"\f483"}.ion-ios-pint:before{content:"\f486"}.ion-ios-pint-outline:before{content:"\f485"}.ion-ios-play:before{content:"\f488"}.ion-ios-play-outline:before{content:"\f487"}.ion-ios-plus:before{content:"\f48b"}.ion-ios-plus-empty:before{content:"\f489"}.ion-ios-plus-outline:before{content:"\f48a"}.ion-ios-pricetag:before{content:"\f48d"}.ion-ios-pricetag-outline:before{content:"\f48c"}.ion-ios-pricetags:before{content:"\f48f"}.ion-ios-pricetags-outline:before{content:"\f48e"}.ion-ios-printer:before{content:"\f491"}.ion-ios-printer-outline:before{content:"\f490"}.ion-ios-pulse:before{content:"\f493"}.ion-ios-pulse-strong:before{content:"\f492"}.ion-ios-rainy:before{content:"\f495"}.ion-ios-rainy-outline:before{content:"\f494"}.ion-ios-recording:before{content:"\f497"}.ion-ios-recording-outline:before{content:"\f496"}.ion-ios-redo:before{content:"\f499"}.ion-ios-redo-outline:before{content:"\f498"}.ion-ios-refresh:before{content:"\f49c"}.ion-ios-refresh-empty:before{content:"\f49a"}.ion-ios-refresh-outline:before{content:"\f49b"}.ion-ios-reload:before{content:"\f49d"}.ion-ios-reverse-camera:before{content:"\f49f"}.ion-ios-reverse-camera-outline:before{content:"\f49e"}.ion-ios-rewind:before{content:"\f4a1"}.ion-ios-rewind-outline:before{content:"\f4a0"}.ion-ios-rose:before{content:"\f4a3"}.ion-ios-rose-outline:before{content:"\f4a2"}.ion-ios-search:before{content:"\f4a5"}.ion-ios-search-strong:before{content:"\f4a4"}.ion-ios-settings:before{content:"\f4a7"}.ion-ios-settings-strong:before{content:"\f4a6"}.ion-ios-shuffle:before{content:"\f4a9"}.ion-ios-shuffle-strong:before{content:"\f4a8"}.ion-ios-skipbackward:before{content:"\f4ab"}.ion-ios-skipbackward-outline:before{content:"\f4aa"}.ion-ios-skipforward:before{content:"\f4ad"}.ion-ios-skipforward-outline:before{content:"\f4ac"}.ion-ios-snowy:before{content:"\f4ae"}.ion-ios-speedometer:before{content:"\f4b0"}.ion-ios-speedometer-outline:before{content:"\f4af"}.ion-ios-star:before{content:"\f4b3"}.ion-ios-star-half:before{content:"\f4b1"}.ion-ios-star-outline:before{content:"\f4b2"}.ion-ios-stopwatch:before{content:"\f4b5"}.ion-ios-stopwatch-outline:before{content:"\f4b4"}.ion-ios-sunny:before{content:"\f4b7"}.ion-ios-sunny-outline:before{content:"\f4b6"}.ion-ios-telephone:before{content:"\f4b9"}.ion-ios-telephone-outline:before{content:"\f4b8"}.ion-ios-tennisball:before{content:"\f4bb"}.ion-ios-tennisball-outline:before{content:"\f4ba"}.ion-ios-thunderstorm:before{content:"\f4bd"}.ion-ios-thunderstorm-outline:before{content:"\f4bc"}.ion-ios-time:before{content:"\f4bf"}.ion-ios-time-outline:before{content:"\f4be"}.ion-ios-timer:before{content:"\f4c1"}.ion-ios-timer-outline:before{content:"\f4c0"}.ion-ios-toggle:before{content:"\f4c3"}.ion-ios-toggle-outline:before{content:"\f4c2"}.ion-ios-trash:before{content:"\f4c5"}.ion-ios-trash-outline:before{content:"\f4c4"}.ion-ios-undo:before{content:"\f4c7"}.ion-ios-undo-outline:before{content:"\f4c6"}.ion-ios-unlocked:before{content:"\f4c9"}.ion-ios-unlocked-outline:before{content:"\f4c8"}.ion-ios-upload:before{content:"\f4cb"}.ion-ios-upload-outline:before{content:"\f4ca"}.ion-ios-videocam:before{content:"\f4cd"}.ion-ios-videocam-outline:before{content:"\f4cc"}.ion-ios-volume-high:before{content:"\f4ce"}.ion-ios-volume-low:before{content:"\f4cf"}.ion-ios-wineglass:before{content:"\f4d1"}.ion-ios-wineglass-outline:before{content:"\f4d0"}.ion-ios-world:before{content:"\f4d3"}.ion-ios-world-outline:before{content:"\f4d2"}.ion-ipad:before{content:"\f1f9"}.ion-iphone:before{content:"\f1fa"}.ion-ipod:before{content:"\f1fb"}.ion-jet:before{content:"\f295"}.ion-key:before{content:"\f296"}.ion-knife:before{content:"\f297"}.ion-laptop:before{content:"\f1fc"}.ion-leaf:before{content:"\f1fd"}.ion-levels:before{content:"\f298"}.ion-lightbulb:before{content:"\f299"}.ion-link:before{content:"\f1fe"}.ion-load-a:before{content:"\f29a"}.ion-load-b:before{content:"\f29b"}.ion-load-c:before{content:"\f29c"}.ion-load-d:before{content:"\f29d"}.ion-location:before{content:"\f1ff"}.ion-lock-combination:before{content:"\f4d4"}.ion-locked:before{content:"\f200"}.ion-log-in:before{content:"\f29e"}.ion-log-out:before{content:"\f29f"}.ion-loop:before{content:"\f201"}.ion-magnet:before{content:"\f2a0"}.ion-male:before{content:"\f2a1"}.ion-man:before{content:"\f202"}.ion-map:before{content:"\f203"}.ion-medkit:before{content:"\f2a2"}.ion-merge:before{content:"\f33f"}.ion-mic-a:before{content:"\f204"}.ion-mic-b:before{content:"\f205"}.ion-mic-c:before{content:"\f206"}.ion-minus:before{content:"\f209"}.ion-minus-circled:before{content:"\f207"}.ion-minus-round:before{content:"\f208"}.ion-model-s:before{content:"\f2c1"}.ion-monitor:before{content:"\f20a"}.ion-more:before{content:"\f20b"}.ion-mouse:before{content:"\f340"}.ion-music-note:before{content:"\f20c"}.ion-navicon:before{content:"\f20e"}.ion-navicon-round:before{content:"\f20d"}.ion-navigate:before{content:"\f2a3"}.ion-network:before{content:"\f341"}.ion-no-smoking:before{content:"\f2c2"}.ion-nuclear:before{content:"\f2a4"}.ion-outlet:before{content:"\f342"}.ion-paintbrush:before{content:"\f4d5"}.ion-paintbucket:before{content:"\f4d6"}.ion-paper-airplane:before{content:"\f2c3"}.ion-paperclip:before{content:"\f20f"}.ion-pause:before{content:"\f210"}.ion-person:before{content:"\f213"}.ion-person-add:before{content:"\f211"}.ion-person-stalker:before{content:"\f212"}.ion-pie-graph:before{content:"\f2a5"}.ion-pin:before{content:"\f2a6"}.ion-pinpoint:before{content:"\f2a7"}.ion-pizza:before{content:"\f2a8"}.ion-plane:before{content:"\f214"}.ion-planet:before{content:"\f343"}.ion-play:before{content:"\f215"}.ion-playstation:before{content:"\f30a"}.ion-plus:before{content:"\f218"}.ion-plus-circled:before{content:"\f216"}.ion-plus-round:before{content:"\f217"}.ion-podium:before{content:"\f344"}.ion-pound:before{content:"\f219"}.ion-power:before{content:"\f2a9"}.ion-pricetag:before{content:"\f2aa"}.ion-pricetags:before{content:"\f2ab"}.ion-printer:before{content:"\f21a"}.ion-pull-request:before{content:"\f345"}.ion-qr-scanner:before{content:"\f346"}.ion-quote:before{content:"\f347"}.ion-radio-waves:before{content:"\f2ac"}.ion-record:before{content:"\f21b"}.ion-refresh:before{content:"\f21c"}.ion-reply:before{content:"\f21e"}.ion-reply-all:before{content:"\f21d"}.ion-ribbon-a:before{content:"\f348"}.ion-ribbon-b:before{content:"\f349"}.ion-sad:before{content:"\f34a"}.ion-sad-outline:before{content:"\f4d7"}.ion-scissors:before{content:"\f34b"}.ion-search:before{content:"\f21f"}.ion-settings:before{content:"\f2ad"}.ion-share:before{content:"\f220"}.ion-shuffle:before{content:"\f221"}.ion-skip-backward:before{content:"\f222"}.ion-skip-forward:before{content:"\f223"}.ion-social-android:before{content:"\f225"}.ion-social-android-outline:before{content:"\f224"}.ion-social-angular:before{content:"\f4d9"}.ion-social-angular-outline:before{content:"\f4d8"}.ion-social-apple:before{content:"\f227"}.ion-social-apple-outline:before{content:"\f226"}.ion-social-bitcoin:before{content:"\f2af"}.ion-social-bitcoin-outline:before{content:"\f2ae"}.ion-social-buffer:before{content:"\f229"}.ion-social-buffer-outline:before{content:"\f228"}.ion-social-chrome:before{content:"\f4db"}.ion-social-chrome-outline:before{content:"\f4da"}.ion-social-codepen:before{content:"\f4dd"}.ion-social-codepen-outline:before{content:"\f4dc"}.ion-social-css3:before{content:"\f4df"}.ion-social-css3-outline:before{content:"\f4de"}.ion-social-designernews:before{content:"\f22b"}.ion-social-designernews-outline:before{content:"\f22a"}.ion-social-dribbble:before{content:"\f22d"}.ion-social-dribbble-outline:before{content:"\f22c"}.ion-social-dropbox:before{content:"\f22f"}.ion-social-dropbox-outline:before{content:"\f22e"}.ion-social-euro:before{content:"\f4e1"}.ion-social-euro-outline:before{content:"\f4e0"}.ion-social-facebook:before{content:"\f231"}.ion-social-facebook-outline:before{content:"\f230"}.ion-social-foursquare:before{content:"\f34d"}.ion-social-foursquare-outline:before{content:"\f34c"}.ion-social-freebsd-devil:before{content:"\f2c4"}.ion-social-github:before{content:"\f233"}.ion-social-github-outline:before{content:"\f232"}.ion-social-google:before{content:"\f34f"}.ion-social-google-outline:before{content:"\f34e"}.ion-social-googleplus:before{content:"\f235"}.ion-social-googleplus-outline:before{content:"\f234"}.ion-social-hackernews:before{content:"\f237"}.ion-social-hackernews-outline:before{content:"\f236"}.ion-social-html5:before{content:"\f4e3"}.ion-social-html5-outline:before{content:"\f4e2"}.ion-social-instagram:before{content:"\f351"}.ion-social-instagram-outline:before{content:"\f350"}.ion-social-javascript:before{content:"\f4e5"}.ion-social-javascript-outline:before{content:"\f4e4"}.ion-social-linkedin:before{content:"\f239"}.ion-social-linkedin-outline:before{content:"\f238"}.ion-social-markdown:before{content:"\f4e6"}.ion-social-nodejs:before{content:"\f4e7"}.ion-social-octocat:before{content:"\f4e8"}.ion-social-pinterest:before{content:"\f2b1"}.ion-social-pinterest-outline:before{content:"\f2b0"}.ion-social-python:before{content:"\f4e9"}.ion-social-reddit:before{content:"\f23b"}.ion-social-reddit-outline:before{content:"\f23a"}.ion-social-rss:before{content:"\f23d"}.ion-social-rss-outline:before{content:"\f23c"}.ion-social-sass:before{content:"\f4ea"}.ion-social-skype:before{content:"\f23f"}.ion-social-skype-outline:before{content:"\f23e"}.ion-social-snapchat:before{content:"\f4ec"}.ion-social-snapchat-outline:before{content:"\f4eb"}.ion-social-tumblr:before{content:"\f241"}.ion-social-tumblr-outline:before{content:"\f240"}.ion-social-tux:before{content:"\f2c5"}.ion-social-twitch:before{content:"\f4ee"}.ion-social-twitch-outline:before{content:"\f4ed"}.ion-social-twitter:before{content:"\f243"}.ion-social-twitter-outline:before{content:"\f242"}.ion-social-usd:before{content:"\f353"}.ion-social-usd-outline:before{content:"\f352"}.ion-social-vimeo:before{content:"\f245"}.ion-social-vimeo-outline:before{content:"\f244"}.ion-social-whatsapp:before{content:"\f4f0"}.ion-social-whatsapp-outline:before{content:"\f4ef"}.ion-social-windows:before{content:"\f247"}.ion-social-windows-outline:before{content:"\f246"}.ion-social-wordpress:before{content:"\f249"}.ion-social-wordpress-outline:before{content:"\f248"}.ion-social-yahoo:before{content:"\f24b"}.ion-social-yahoo-outline:before{content:"\f24a"}.ion-social-yen:before{content:"\f4f2"}.ion-social-yen-outline:before{content:"\f4f1"}.ion-social-youtube:before{content:"\f24d"}.ion-social-youtube-outline:before{content:"\f24c"}.ion-soup-can:before{content:"\f4f4"}.ion-soup-can-outline:before{content:"\f4f3"}.ion-speakerphone:before{content:"\f2b2"}.ion-speedometer:before{content:"\f2b3"}.ion-spoon:before{content:"\f2b4"}.ion-star:before{content:"\f24e"}.ion-stats-bars:before{content:"\f2b5"}.ion-steam:before{content:"\f30b"}.ion-stop:before{content:"\f24f"}.ion-thermometer:before{content:"\f2b6"}.ion-thumbsdown:before{content:"\f250"}.ion-thumbsup:before{content:"\f251"}.ion-toggle:before{content:"\f355"}.ion-toggle-filled:before{content:"\f354"}.ion-transgender:before{content:"\f4f5"}.ion-trash-a:before{content:"\f252"}.ion-trash-b:before{content:"\f253"}.ion-trophy:before{content:"\f356"}.ion-tshirt:before{content:"\f4f7"}.ion-tshirt-outline:before{content:"\f4f6"}.ion-umbrella:before{content:"\f2b7"}.ion-university:before{content:"\f357"}.ion-unlocked:before{content:"\f254"}.ion-upload:before{content:"\f255"}.ion-usb:before{content:"\f2b8"}.ion-videocamera:before{content:"\f256"}.ion-volume-high:before{content:"\f257"}.ion-volume-low:before{content:"\f258"}.ion-volume-medium:before{content:"\f259"}.ion-volume-mute:before{content:"\f25a"}.ion-wand:before{content:"\f358"}.ion-waterdrop:before{content:"\f25b"}.ion-wifi:before{content:"\f25c"}.ion-wineglass:before{content:"\f2b9"}.ion-woman:before{content:"\f25d"}.ion-wrench:before{content:"\f2ba"}.ion-xbox:before{content:"\f30c"} diff --git a/css/wand.png b/css/wand.png new file mode 100644 index 000000000..55ad3e079 Binary files /dev/null and b/css/wand.png differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..a269cf2e5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,4 @@ +eve: + build: . + ports: + - 18080:8080 \ No newline at end of file diff --git a/examples/CRM.eve b/examples/CRM.eve new file mode 100644 index 000000000..5b6a4b0fe --- /dev/null +++ b/examples/CRM.eve @@ -0,0 +1,423 @@ +# Contact Application + +This application demonstrates some basic patterns you might want to use in a mobile application or website. These concepts include: + +- application architecture +- navigating between pages +- displaying lists of information +- joining and filtering data +- reusing interface components +- responding to events + +## The App Record + +`#app` stores some useful state for the application, including the current user, the current page, and the current contact. + +``` +commit + [#app] +``` + +Set some initial state on `#app` and mark it as `#init`. This block sets the start time of the app, so `#time` is brought into the search. Since we only want this to happen once, we search for `#app` that is not `#init`. Then, we mark the `#app` as `#init` to prevent the block from firing again due to a time update. + +``` +search + app = [#app] + [#time day month year hours minutes seconds ampm] + not(app = [#init]) + user = [#user name: "Corey Montella"] + //user = [#user name: "Eric Hoffman"] + +commit + app <- [#init user + page: [#about contact: user] + start-time: [#start-time day month year hours minutes seconds ampm]] +``` + +### Main App + +The main application is a shell for all other pages in the app. This component draws the main interface, navigation buttons, and provides a "content" div into which other pages are injected. + +``` +search + app = [#app user] + [#time time-string] + // Handle the case when the user has no threads + threads = if app.unread then app.unread + else 0 + // If the user is looking at a contact, display that contact's avatar and info. Otherwise, display the user's own avatar. + current-contact = if app.contact then app.contact + else user + +bind @browser + [#link rel: "stylesheet" href: "examples/css/crm.css"] + [#div #container class: "container" children: + [#div class: "scroll" children: + [#div class: "banner"] + [#div class: "bio-info" children: + [#div class: "avatar-container" children: + [#img class: "avatar", src: current-contact.avatarURL]] + [#div class: "name2" text: "{{current-contact.name}}"] + [#div class: "info" text: "{{current-contact.title}}"] + [#div class: "info" text: "{{time-string}}, {{current-contact.location}}"]] + [#div #content class: "content"]] + [#div class: "navigation" children: + [#div #about #nav class: "nav-button middle", children: + [#div class: "icon ion-person"] + [#div class: "label" text: "profile"]] + [#div #threads #nav class: "nav-button middle", children: + [#div class: "icon ion-chatboxes"] + [#div class: "bubble" text: threads] + [#div class: "label" text: "messages"]] + [#div #contacts #nav class: "nav-button middle" children: + [#div class: "icon ion-person-stalker" text: ""] + [#div class: "label" text: "contacts"]] + [#div #more #nav class: "nav-button" children: + [#div class: "icon ion-android-more-horizontal" text: ""] + [#div class: "label" text: "more"]]]] +``` + +As part of the main application, we display a count of unread messages in the navigation bar: + +``` +search + app = [#app user] + threads = [#thread users: user, messages] + messages = [#unread] + +bind + app.unread := count[given: messages] +``` + +## Pages + +The application is composed of various pages, that are injected into the main content area depending on the contents of `#app.page`. Only one page should be displayed at a time. + +### About Page + +Displays information relating to a contact. This page is constructed in three parts. The first part displays phone and email for the contact: + +``` +search @browser @session + content = [#content] + [#app page: [#about contact]] + +bind @browser @session + content.children := [#div class: "about" children: + [#div class: "about-line", children: + [#span class: "about-label", text: "Phone"] + [#span text: contact.phone]] + [#div class: "about-line", children: + [#span class: "about-label", text: "Email"] + [#span text: contact.email]] + [#div #recent-contacts children: + [#h3 text: "All Contacts ({{count[given: contact.contacts]}})"] + [#img #contact contact: contact.contacts, class: "recent-avatar" src: contact.contacts.avatarURL]]] +``` + +If the user has threads, then recent contacts are displayed: + +``` +search @browser @session + recent-div = [#recent-contacts] + [#app page: [#about contact], user] + threads = [#thread users: contact messages] + recent = threads.users != contact + +bind @browser @session + recent-div.children += [#div children: + [#h3 text: "Recent Contacts ({{count[given: recent]}})"] + [#img class: "recent-avatar" src: recent.avatarURL]] +``` + +If the user has no threads and is looking at his own about page, a message is displayed prompting the user to start a conversation with someone: + +``` +search @browser @session + recent-div = [#recent-contacts] + [#app page: [#about contact], user] + user = contact + not([#thread users: user]) + +bind @browser + recent-div.children += [#div children: + [#h3 text: "Recent Contacts (0)"] + [#div #contacts #nav children: + [#div class: "plus" text: "+"] + [#div text: "Start a Thread"]]] +``` + +If there is no contact for the about page, then use the current user's info + +``` +search + [#app page user] + page = [#about] + not(page = [contact]) + +commit + page.contact := user +``` + +### Messages Page + +Each message is displayed with the user's avatar and name. An abbreviated timestamp is also displayed. + +``` +search @browser @session + content = [#content] + app = [#app page: [#thread thread]] + msgs = thread.messages + name = if msgs.sender.name = app.user.name then "Me" + else msgs.sender.name + msgs.time = [timestamp time-string] + +bind @browser + content.children := [#div class: "flex-spacer" children: + [#div #convo class: "convo" children: + [#div sort: msgs.time.timestamp class: "msg" message: msgs children: + [#img class: "msg-avatar" src: msgs.sender.avatarURL] + [#span #contact contact: msgs.sender, class: "msg-name" text: name] + [#span class: "msg-time" text: time-string] + [#div class: "msg-text" text: msgs.text]]] + [#input #send-message thread class: "msg-input"]] +``` + +### Threads Page + +Each thread is shown with the contact's avatar and name, and the number of messages in the thread. + +``` +search @browser @session + content = [#content] + app = [#app page: [#threads], user] + thread = [#thread users: user, messages] + contacts = thread.users != app.user + message-count = count[given: thread.messages, per: thread.users] + +bind @browser + content.children := [#div thread class: "thread" children: + [#div #thread thread, class: "thread-box" children: + [#img class: "msg-avatar" src: contacts.avatarURL] + [#div class: "msg-name" text: contacts.name] + [#div text: "{{message-count}} messages"]] + [#div #archive-button thread class: "ion-archive"]] +``` + +If the user has no threads, display a message prompting the user to start a conversation with a contact + +``` +search @browser @session + content = [#content] + app = [#app page: [#threads], user] + not(threads = [#thread users: user, messages]) + +bind @browser + content.children := [#div #contacts #nav class: "button", text: "Start a Thread"] +``` + +### Contacts Page + +Contacts are shown with all their contact details. Clicking on a contact opens up a detailed contact page. + +``` +search @browser @session + content = [#content] + [#app user page: [#contacts]] + +bind @browser @session + content.children := [#div #contact contact: user.contacts, class: "contact" contact: user.contacts, children: + [#img class: "contact-avatar" src: user.contacts.avatarURL] + [#div class: "contact-name", text: user.contacts.name] + [#div text: "Location: {{user.contacts.location}}"] + [#div text: "Phone: {{user.contacts.phone}}"] + [#div text: "Email: {{user.contacts.email}}"]] +``` + +### More Page + +More information about Eve! + +``` +search @browser @session + content = [#content] + [#app page: [#more]] + +bind @browser + content.children := [#div #more class: "more" children: + [#h2 text: "Learn more about Eve"] + [#ul children: + [#li children: [#a href: "http://witheve.com" text: "Homepage"]] + [#li children: [#a href: "https://witheve.github.io/docs/tutorials/quickstart/" text: "Quick Start Tutorial"]] + [#li children: [#a href: "http://github.com/witheve" text: "GitHub Repository"]]] + [#h2 text: "Join the Community"] + [#ul children: + [#li children: [#a href: "http://blog.witheve.com" text: "Development Diary"]] + [#li children: [#a href: "https://github.com/witheve/rfcs" text: "Request for Comments"]] + [#li children: [#a href: "https://groups.google.com/forum/#!forum/eve-talk" text: "Mailing List"]] + [#li children: [#a href: "https://twitter.com/with_eve" text: "Twitter"]]]] +``` + +## Events + +### Set current page + +When the user clicks on a `#nav` button, set the app page to that element. We use this in subsequent blocks to fill the `#content` area of the app. + +``` +search @event @browser @session + [#click element] + element = [#nav] + app = [#app user] + +commit + app.page := [tag: element.tag] + app.contact := none +``` + +### Display contact + +When the user clicks on a contact's name, their "About" page is displayed. Otherwise, the user's own "About" page is displayed. + +``` +search @event @browser @session + [#click element: [#contact contact]] + app = [#app] + +commit + app.contact := contact + app.page := [#about contact] +``` + +### Display messages + +Messages are displayed for a current thread. By displaying messages, they are automatically marked as read, which decrements the count on the navigation bar. + +``` +search @event @browser @session + [#click element: [#thread thread]] + app = [#app] + +commit + app.page := [#thread thread] + thread.messages -= #unread +``` + +### Send a message + +When the user presses "enter" in the message input box, a message is added to the current thread, with the current time. This event should also clear the input box. + +``` +search @event @browser @session + [#keydown key: "enter" element: input] + input = [#send-message thread value] + [#app user] + time = [#time timestamp time-string] + +commit @browser + input.value := "" + thread.messages += [#message sender: user, time: [timestamp time-string], text: value] +``` + +### Archive a thread + +When the user archives a thread, the `#archive` tag is added to that thread, which then exludes it from display in the main thread list. Archiving a thread also marks all unread messages as read. + +- TODO Add a place to see all archived threads + +``` +search @event @browser @session + [#click element: [#archive-button thread]] + messages = thread.messages + +commit @session + thread += #archived + messages -= #unread +``` + +## Test Data + +The users and messages in this application are fabricated. + +``` +search + t1 = 1479258949716 + t2 = t1 + 1000 + t3 = t2 + 1000 + t4 = t3 + 1000 + t5 = t4 + 1000 + t6 = t5 + 1000 + time-string = "5:15 PM" + +commit + corey = [#user + name: "Corey Montella" + title: "Software Engineer" + avatarURL: "https://avatars2.githubusercontent.com/u/10619266?v=3&s=466" + location: "San Francisco, CA" + email: "corey@kodowa.com" + phone: "555-555-5555"] + + chris = [#user + name: "Chris Granger" + title: "CEO" + avatarURL: "https://avatars3.githubusercontent.com/u/70036?v=3&s=466" + location: "San Francisco, CA" + email: "chris@kodowa.com" + phone: "555-555-5556"] + + josh = [#user + name: "Josh Cole" + title: "Software Engineer" + avatarURL: "https://avatars2.githubusercontent.com/u/313870?v=3&s=466" + location: "San Francisco, CA" + email: "josh@kodowa.com" + phone: "555-555-5557"] + + rob = [#user + name: "Rob Attorri" + title: "President" + avatarURL: "https://avatars1.githubusercontent.com/u/1314445?v=3&s=466" + location: "San Francisco, CA" + email: "rob@kodowa.com" + phone: "555-555-5558"] + + eric = [#user + name: "Eric Hoffman" + title: "Software Engineer" + avatarURL: "https://avatars3.githubusercontent.com/u/1807982?v=3&s=466" + location: "San Francisco, CA" + email: "eric@kodowa.com" + phone: "555-555-5559"] + + // Add contacts to users + corey.contacts := (chris, josh, rob, eric) + chris.contacts := (corey, josh, rob) + josh.contacts := (corey, rob, eric) + rob.contacts := (corey, josh, chris, eric) + eric.contacts := (josh, rob) + + // Make some threads + [#thread #new-messages users: (corey, chris) messages: + [#message sender: corey, time: [timestamp: t1, time-string], text: "Hey"] + [#message sender: chris, time: [timestamp: t2, time-string], text: "Hey, how are you."] + [#message sender: corey, time: [timestamp: t3, time-string], text: "I'm fine, how are you?"] + [#message #unread sender: chris, time: [timestamp: t4, time-string], text: "Fine as well."] + [#message #unread sender: chris, time: [timestamp: t5, time-string], text: "Glad we got that out of the way!"] + [#message #unread sender: chris, time: [timestamp: t6, time-string], text: "What did you work on yesterday?"]] + + [#thread users: (corey, josh) messages: + [#message sender: josh, time: [timestamp: t1, time-string], text: "Hey"] + [#message sender: corey, time: [timestamp: t2, time-string], text: "What's up Josh?"] + [#message sender: josh, time: [timestamp: t3, time-string], text: "I need to tell you something...."] + [#message sender: corey, time: [timestamp: t4, time-string], text: "Uh oh..."] + [#message sender: corey, time: [timestamp: t5, time-string], text: "Well what is it? Don't leave me hanging!"]] + + [#thread #new-messages users: (corey, rob) messages: + [#message sender: corey, time: [timestamp: t1, time-string], text: "Did Josh tell you what happened?"] + [#message sender: rob, time: [timestamp: t2, time-string], text: "Yeah, don't worry, we took care of it. "] + [#message sender: corey, time: [timestamp: t3, time-string], text: "Well what happened?"] + [#message sender: rob, time: [timestamp: t4, time-string], text: "Like I said, don't worry about it."] + [#message sender: corey, time: [timestamp: t5, time-string], text: "..."] + [#message #unread sender: rob, time: [timestamp: t6, time-string], text: "🔥"]] +``` diff --git a/examples/analyzer.eve b/examples/analyzer.eve new file mode 100644 index 000000000..2b21f04f7 --- /dev/null +++ b/examples/analyzer.eve @@ -0,0 +1,583 @@ +# Eve Analyzer + +## assign variables + +For every variable there is a group + +~~~eve disabled +search + variable = [#variable] + number = random[seed: variable] + +commit + [#group number variable] +~~~ + +~~~eve disabled +search + variable = [#variable not(group)] + group = [#group variable] + +commit + variable.group := group +~~~ + +Handle a constant equivalence + +~~~eve disabled +search + eq = [#equality] + (a, b) = if eq.a.tag, not(eq.b.tag) then (eq.a, eq.b) + else if eq.b.tag, not(eq.a.tag) then (eq.b, eq.a) + +bind + a.constant += b +~~~ + +Handle variable equivalence + +~~~eve disabled +search + [#equality a b] + a.group != b.group + min-a = min[value: a.group.number, given: a.group.number, per: a] + min-b = min[value: b.group.number, given: b.group.number, per: b] + a-group = [#group number: min-a] + b-group = [#group number: min-b] + (new, old) = if min-a < min-b then (a-group, b-group) + else if min-b < min-a then (b-group, a-group) + + var = [#variable group: old] +commit + var.group := new +~~~ + +## associate record-tags to both actions and scans + +Any scan attached to a pattern that is looking for our record-tag is +related to that tag. + +~~~ +search + [#query record-tag] + [#scan entity: [register] attribute: "tag" value: record-tag block] + scan = [#scan entity: [register] block] + +bind + scan.record-tag += record-tag +~~~ + +Any action attached to a pattern that is looking for our record-tag is +related to that tag. + +~~~ +search + [#query record-tag] + [#action entity: [register] attribute: "tag" value: record-tag block] + action = [#action entity: [register] block] + +bind + action.record-tag += record-tag +~~~ + + +Actions that are adding to an entity with our record tag, are also for the +same record-tag. + +~~~ +search + [#query record-tag] + [#scan entity: [register] record-tag block] + action = [#action entity: [register] block] +bind + action.record-tag += record-tag +~~~ + +The inverse is true as well, if an action is for our record tag and the entity +is used in a scan, then that scan is for this tag. + +~~~ +search + [#query record-tag] + [#action entity: [register] record-tag block] + scan = [#scan entity: [register] block] + +bind + scan.record-tag += record-tag +~~~ + +## tag equivalence + +~~~ + search + attribute = "tag" + [#scan block entity: [register] attribute value: tag] + [#action block entity: [register] attribute value: tag2] + + bind + [#tag-equivalence tag tag2] +~~~ + +## unprovided scans + +~~~ eve disabled +search + scan = [#scan record-tag attribute] + not( action = [#action record-tag attribute] + value = if scan.value = action.value then true + else if scan.value = [#variable] then true + else if action.value = [#variable] then true ) + +bind + scan += #unprovided +~~~ + +~~~ eve disabled +search + scan = [#scan #analyzer/any attribute] + not( action = [#action attribute] + value = if scan.value = action.value then true + else if scan.value = [#variable] then true + else if action.value = [#variable] then true ) + +bind + scan += #unprovided +~~~ + +~~~ eve disabled +search + scan = [#scan #unprovided start stop record-tag attribute block] + +bind @editor + [#comment scan | block start stop message: "{{attribute}} is never added to {{record-tag}} records, so this will always be empty"] +~~~ + +# token query + +## query links + +~~~ +search + query = [#query token] + [#link a: to b: token] + +bind + [#query-link token to distance: 1] +~~~ + +~~~ +search + query = [#query token] + link = [#query-link token distance] + [#link a: to b: link.to] + +bind + [#query-link token to distance: distance + 1] +~~~ + +## token -> variable + +~~~ +search + query = [#query token] + [#link a: to b: token] + to = [#variable register block] + token.block = block + +bind + query.register += [token variable: to register block name: to.name] +~~~ + +We can also associate what attribute that variable is writing to if there is one + +~~~ +search + query = [#query register] + action = [#action attribute value: register.variable] + +bind + register.attribute += attribute +~~~ + +## token -> scan + +~~~ +search + query = [#query token] + [#query-link token to] + to = [#scan start stop] + start <= token.start <= stop + +bind + query.scan += to +~~~ + +~~~ +search + [#query token scan: [#scan record-tag attribute value]] + provider = [#action record-tag attribute start stop] + +bind @editor + [#comment provider | start stop kind: "warning" message: "this is providing it" ] + +bind @browser + [#div text: "provided by: {{provider}}"] +~~~ + +~~~ +search + [#query token scan: [#scan record-tag attribute]] + +bind @browser + [#div text: "{{token}} scans #{{record-tag}} {{attribute}}"] +~~~ + +## token -> action + +~~~ +search + query = [#query token] + [#query-link token to] + to = [#action start stop] + start <= token.start <= stop + +bind + query.action += to +~~~ + +~~~ +search + [#query token action: [#action record-tag attribute]] + consumer = [#scan record-tag attribute start stop] + +bind @editor + [#comment consumer | start stop kind: "warning" message: "This is consuming" ] + +bind @browser + [#div text: "consumed by: {{consumer}}"] +~~~ + +~~~ +search + [#query token action: [#action record-tag attribute]] + +bind @browser + [#div text: "{{token}} actions #{{record-tag}} {{attribute}}"] +~~~ + +## token -> record + +~~~ +search + query = [#query] + entity = if query.scan then query.scan.entity + else if query.action then query.action.entity + record = [#record entity start stop] + +bind + query <- [entity entity-register: entity.register] +~~~ + + +## find variables related to a span + +Determine the kind of span we're looking for + +~~~ +search + query = [#findRelated for: "span" span] + kind = if span = [#action] then "action" + else if span = [#scan] then "scan" + else if span = [#record kind] then kind + +bind + query.kind := kind +~~~ + +If the span is an action, we need to find the variables related to both the +value and the entity + +~~~ +search + query = [#findRelated for: "span" span kind] + span = [tag: kind entity value] + variable = if entity = [#variable] then entity + if value = [#variable] then value + [#link a: variable b: token] + +bind + query.variable += token +~~~ + +If the span is a record, we need to find all the actions directly related to it + +~~~ +search + query = [#findRelated for: "span" span] + span = [#record entity start stop kind] + scan = [tag: kind, entity] + scan.start >= start + scan.stop <= stop + scan.value = [#variable] + [#link a: scan.value b: token] + [#link a: entity b: entity-token] + +bind + query.variable += entity-token + query.variable += token +~~~ + +if one of the variables we found is the entity for another scan/action, then we need +to add him to the list of things we want to find relateds for + +~~~ +search + query = [#findRelated for: "span" kind variable] + [#link a: entity b: variable] + span = [tag: kind entity] + +bind + query.span += span +~~~ + +## findValue + +~~~ +search + query = [#findValue] +~~~ + +## findCardinality + +~~~ +search + query = [#findCardinality] +~~~ + +## find spans related to a variable + +~~~ +search + query = [#findRelated for: "variable" variable: token] + [#link a: variable b: token] + span = if span.entity = variable then span + if span.value = variable then span + +bind + query.span += span +~~~ + +## recordId -> build-node + +~~~ +search + query = [#findSource recordId not(attribute)] + +search @evaluation-session @evaluation-browser + recordId = [tag] + +bind + query.cool += tag +~~~ + +Find out what nodes contribute to a record + +~~~ +search + query = [#findSource recordId not(attribute)] + +search @evaluation-session @evaluation-browser + lookup[record: recordId, attribute, value, node] + +bind + query.node += node +~~~ + +Find out what nodes contribute to a specific attribute on a record + +~~~ +search + query = [#findSource recordId attribute] + +search @evaluation-session @evaluation-browser + lookup[record: recordId, attribute, value, node] + +bind + query.node += node +~~~ + +We may also want to lookup what node created a complete eav + +~~~ +search + query = [#findSource recordId attribute value] + +search @evaluation-session @evaluation-browser + lookup[record: recordId, attribute, value, node] + +bind + query.node += node +~~~ + +## scan to block + +~~~ +search + query = [#findSource span: [block]] + +bind + query.block += block +~~~ + +## nodeId -> creator (aka findSource) + +~~~ +search + query = [#findSource node] + action = [#action entity build-node: node block] + span = if query.attribute then action + else if record = [#record entity] then record + else action + +bind + query.source += [span block start: span.start, stop: span.stop] +~~~ + +## build-node to creator + +~~~ +search + query = [#query build-node] + [#action entity build-node] + record = [#record entity] + +commit + query.pattern := record +~~~ + +## findRecordsFromToken + +Add the #query tag so that we find all the scans/actions related to the given +token + +~~~ +search + query = [#findRecordsFromToken] + +commit + query += #query +~~~ + +~~~ +search + query = [#findRecordsFromToken token] + [#query-link token to] + to = [#action build-node] + +commit + query.build-node += build-node +~~~ + +~~~ +search + query = [#findRecordsFromToken token] + [#query-link token to] + to = [#action build-node] + +search @evaluation-session @evaluation-browser + lookup[node: build-node, record] + +commit + query.record += record +~~~ + +## findAffector + +Store what record-tag we need to compute providers and consumers for + +~~~ +search + query = [#findAffector recordId] + +search @evaluation-session @evaluation-browser + tag = recordId.tag + +bind + query += #query + query.record-tag += tag +~~~ + +Find affectors based on what providers and consumers exist + +~~~ +search + query = [#findAffector recordId attribute record-tag] + scan = [#scan entity: [register] record-tag block] + action = [#action entity: [register] block attribute] + final = if record = [#record entity: [register] start stop] + action.start >= start + action.stop <= stop then record + else action + +bind + query.affector := [action: final, block] +~~~ + +~~~ +search + query = [#findAffector recordId attribute] + //scan = [#scan entity: [register] record-tag: tag block] + //action = [#action entity: [register] attribute block ] + +search @evaluation-session @evaluation-browser + recordId = [tag] + +bind @browser + //[#div text: "yo {{tag}}"] +~~~ + + +## findRootDrawers + +~~~ +search + query = [#findRootDrawers] + action = [#action block entity: [register] scopes: "browser"] + not([#scan block entity: [register]]) + not([#action block value: [register]]) + final = if record = [#record entity: [register] start stop] + action.start >= start + action.stop <= stop then record + else action + +commit + query.drawer += [id: final start: final.start stop: final.stop] +~~~ + + +## findMaybeDrawers + +~~~ +commit + [#drawing-tag tag: "div"] + [#drawing-tag tag: "span"] + [#drawing-tag tag: "img"] + [#drawing-tag tag: "input"] + [#drawing-tag tag: "button"] +~~~ + +~~~ +search + query = [#findMaybeDrawers] + [#drawing-tag tag: drawing-tag] + action = [#action entity: [register] block attribute: "tag" value: drawing-tag] + not(action.scopes = "browser") + final = if record = [#record entity: [register] start stop] + action.start >= start + action.stop <= stop then record + else action + +commit + query.drawer += [id: final start: final.start stop: final.stop ] +~~~ + diff --git a/examples/attendance-bug.eve b/examples/attendance-bug.eve new file mode 100644 index 000000000..ab3fc2850 --- /dev/null +++ b/examples/attendance-bug.eve @@ -0,0 +1,111 @@ +# Attendance Bug + +This is a simple webapp with an equally simple issue: When navigated to the "attendance" page, nothing shows up! It provides a playground for using the inspector (the magic wand button in the top right of the editor). By clicking the inspect button and then anything in the IDE, Eve will tell you what it knows about your target. From there, it works with you to understand both what was intended and what's actually happening. The inspector is still in its infancy, so the UX is pretty rough. Any and all feedback is welcome on the [mailing list](https://groups.google.com/forum/#!forum/eve-talk). + +### Setup + +Create some students, teachers, syllabi, and schools. + +~~~ +commit + middlington = [#school name: "Middlington Jr. High"] + + [#student name: "John" school: middlington class: 1 grade: 85 absences: 1] + [#student name: "Beth" school: middlington class: 1 grade: 78 absences: 4] + [#student name: "Jorge" school: middlington class: 2 grade: 92 absences: 1] + [#student name: "Rin" school: middlington class: 1 grade: 91 absences: 0] + + user = [#teacher name: "George" school: middlington class: 1] + [#teacher name: "Alice" school: middlington class: 2] + + [#syllabus name: "Intro to Introductory Courses" class: 1 | assignment: ("homework 1", "homework 2", "quiz 1", "finals")] + [#syllabus name: "Advanced Rocket Surgery" class: 2 | assignment: ("self study", "group project", "finals")] + + [#app page: "grades" user] +~~~ + +### Events + +When a `#nav` button is clicked, update the app's current page. + +~~~ +search @event + [#click #direct-target element] + +search @browser + element = [#button #nav app text: page] + +commit + app.page := page +~~~ + +### Drawing + +Draw the page template. The specific page will be drawn into `#content` later on. + +~~~ +search + app = [#app] + +bind @browser + app <- [#div children: + [#div style: [margin-bottom: 20] children: + [#button #nav app text: "grades"] + [#button #nav app text: "syllabus"] + [#button #nav app text: "attendance"]] + [#div #content app style: [min-height: 500] sort: 2]] +~~~ + +### Pages + +When the page is "grades", show the student's grades for the current teacher. + +~~~ +search + app = [#app page: "grades" user: teacher] + student = [#student name school: teacher.school class: teacher.class grade] + ix = sort[value: name] + +search @browser + content = [#content app] + +bind @browser + content <- [children: + [#header sort: 0 text: "Grades for {{teacher.name}}'s class"] + [#div sort: ix text: "{{name}}'s grade is {{grade}}"]] + +~~~ + +When the page is "syllabus", show the syllabus for the current teacher's class. + +~~~ +search + app = [#app page: "syllabus" user: teacher] + syllabus = [#syllabus class: teacher.class assignment] + +search @browser + content = [#content app] + +bind @browser + content <- [children: + [#header sort: 0 text: "Syllabus for {{syllabus.name}}"] + [#div text: assignment]] +~~~ + +When the page is "attendance", show each student's number of absences. + +~~~ +search + app = [#app page: "attendance" user: teacher] + teacher.school = [#school name] + student = [#student name school: teacher.school class: teacher.class grade] + ix = sort[value: name] + +search @browser + content = [#content app] + +bind @browser + content <- [children: + [#header sort: 0 text: "Attendance for {{name}}"] + [#div sort: ix text: "{{name}}'s grade is {{grade}}"]] +~~~ diff --git a/examples/bar-graph.eve b/examples/bar-graph.eve new file mode 100644 index 000000000..055314c30 --- /dev/null +++ b/examples/bar-graph.eve @@ -0,0 +1,37 @@ +# Pet Lengths + +Demonstration of the bar graph view. + +Create some pets with rigorously measured lengths. + +~~~ +commit + [#pet name: "koala" length: 7] + [#pet name: "cat" length: 3] + [#pet name: "whale" length: 12] + [#pet name: "dog" length: 14] + [#pet name: "orangutan" length: 9] + [#pet name: "lemur" length: 5] +~~~ + +Each pet is a single bar on our graph. The bar's label is the pet's name, it's height is the pet's length. The sort property tells the bar graph to draw the bar in alphabetical order based on the pets' names. + +~~~ +search + [#pet name length] + ix = sort[value: name] + +bind @view + [#bar-graph | bar: [label: name height: length sort: ix]] +~~~ + +A team of scientists work tirelessly around the clock to keep the mysterious whargarbbl's length updated. Since each bar in the bar chart is bound above, the whargarbbl's bar will stay in sync. + +~~~ +search + [#time minutes seconds] + length = random[seed: minutes * seconds] * 30 + +bind + [#pet name: "whargarbbl" length] +~~~ diff --git a/examples/clock.eve b/examples/clock.eve new file mode 100644 index 000000000..0f9d4ca7f --- /dev/null +++ b/examples/clock.eve @@ -0,0 +1,25 @@ +# Clock + +Draw an appropriately positioned line into `#clock-hand`s. + +~~~ +search @browser + hand = [#clock-hand degrees length] + x2 = 50 + (length * sin[degrees]) + y2 = 50 - (length * cos[degrees]) +bind @browser + hand <- [#line, x1: 50, y1: 50, x2, y2] +~~~ + +Draw a clock using SVG. We find the angle for each hand using time and let the `#clock-hand` block take care of the rest. + +~~~ +search + [#time hours minutes seconds] +bind @browser + [#svg viewBox: "0 0 100 100", width: "300px", children: + [#circle cx: 50, cy: 50, r: 45, fill: "#0B79CE"] + [#clock-hand #hour-hand degrees: 30 * hours, length: 30, stroke: "#023963"] + [#clock-hand #minute-hand degrees: 6 * minutes, length: 40, stroke: "#023963"] + [#clock-hand #second-hand degrees: 6 * seconds, length: 40, stroke: "#ce0b46"]] +~~~ diff --git a/examples/counter.eve b/examples/counter.eve new file mode 100644 index 000000000..db1662984 --- /dev/null +++ b/examples/counter.eve @@ -0,0 +1,63 @@ +# Counter + +This program demonstrates: + +- responding to events +- drawing elements on the screen +- reusing elements programatically +- extending an element + +### Increment a Counter + +Each button uses the referenced counter to increment itself. We need to search on three databases to accomplish this goal: + +- `#click` is in `@event`. +- `#button` is in `@browser` +- `counter.count` is in `@session` + +~~~ +search @event @browser @session + [#click #direct-target element: [#button diff counter]] + +commit + counter.count := counter.count + diff +~~~ + +### Build a Counter + +For every `#counter`, we create a `#div` that contains the elements that draw the counter. The counters are added to the root of the DOM, but you could add them to a particular element in the DOM by specifying a parent element. + +~~~ +search + counter = [#counter count] + +bind @browser + [#div counter class: "flex-row" style: [flex: "0 0 auto", width: 80] children: + [#button text: "-", diff: -1, counter] + [#div text: count counter style: [padding: "0 6px" flex: 1]] + [#button text: "+", diff: 1, counter]] +~~~ + +Add some counters programatically. To make `n` unique and independent counters, we need to add something to the committed counter that makes it unique. Since `i` = `{1, 2, 3, 4}`, when we add it to the counter record we get 4 different counters. We also add one `#fancy` counter, which is a standard counter with new styling. + +~~~ +search + i = range[from: 1 to: 4] + +commit + [#counter i count: 0] + [#counter #fancy count: 0] +~~~ + +### Extend the Counter + +This block says: "For every div with a counter that is tagged fancy, add a style with a black background and pink text". Let's break it down. We search for all `#div`s with a counter attribute. The counter is constrained to be only counters with a `#fancy` tag. Then we bind a new style to each. + +~~~ +search @browser @session + counter-element = [#div counter] + counter.tag = "fancy" + +bind @browser @session + counter-element.style += [background: "black" color: "#FFD0E0"] +~~~ diff --git a/examples/css/crm.css b/examples/css/crm.css new file mode 100644 index 000000000..ff0cfd44b --- /dev/null +++ b/examples/css/crm.css @@ -0,0 +1,270 @@ +h3, h4 { font-weight:normal; font-size:16px; } + +.container { + box-shadow: 0 3px 8px #bbb; + width: 400px; + height: 711px; + background: white; + font-family: sans-serif; + font-size: 14px; + display: flex; + flex-direction: column; +} + +.scroll { + flex: 1; + overflow: auto; + display:flex; + flex-direction:column; +} + +.avatar { + width: 128; + height: 128; +} + +.banner { + background: linear-gradient(-90deg, rgb(74,64,136), rgb(0,158,224)); + width: 100%; + height: 150px; + flex:none; +} + +.avatar-container { + border-radius: 6px; + width: 128px; + height: 128px; + overflow: hidden; + border: 5px solid white; + margin: auto; + margin-top: -60px; +} + +.name2 { + width: 100%; + text-align: center; + color: rgb(11,11,37); + font-family: sans-serif; + font-size: 20px; + font-weight: bold; + margin-top: 10px; +} + +.info { + width: 100%; + text-align: center; + color: rgb(147,167,178); + color: #999; + font-family: sans-serif; + font-size: 14px; + margin-top: 5px; +} + +.navigation { + display: flex; + flex:none; + border-top: 1px solid #eee; + padding:5px 0; +} + +.nav-button { + position:relative; + flex-grow: 1; + text-align: center; + height: 60px; + cursor: pointer; + color: #999; +} + +.icon { + font-size: 30px; + padding: 3px; + height: 35px; + margin-top:2px; +} + +.middle { + border-right: 1px solid #eee; +} + +.bubble { + position: absolute; + top:8px; + left: 56px; + color: white; + font-weight: bold; +} + +.content { + margin-top: 10px; + padding: 0px; + flex-grow: 1; + display: flex; + flex-direction: column; +} + +.convo { + height: 280px; + margin-bottom: 10px; + padding: 10px 20px; + overflow: auto; +} + +.msg-avatar { + height: 40px; + width: 40px; + margin-right: 15px; + float: left; + border-radius: 4px; + border: 1px solid #ddd; +} + +.msg-name { + font-weight: bold; + color: #555; + margin-top:2px; + margin-bottom:2px; +} + +.msg { + margin-bottom: 10px; + min-height: 40px; + flex:none; +} + +.thread:first-child { margin-top:30px; border-top:1px solid #eee; } + +.thread { + padding: 15px 20px; + display: flex; + flex-direction: row; + border-bottom: 1px solid #eee; + align-items: center; + color: #999; +} + +.thread img {} + +.thread-box { + flex-grow: 1; +} + +.archive { + cursor: pointer; + color: #999; + font-size: 25px; +} + +.contact-avatar { + width: 80px; + height: 80px; + float: left; + margin-right: 10px; + border-radius: 4px; +} + +.contact-name { + font-weight: bold; + font-size: 18px; + margin-top:3px; + margin-bottom: 5px; + color: #666; +} + +.contact { + flex: none; + padding: 15px 20px; + border-bottom: 1px solid #eee; + color: #999; +} + +.contact:first-child { margin-top: 20px; border-top:1px solid #eee; } + +.recent-avatar { + width: 40px; + height: 40px; + margin-right: 10px; +} + +.msg-input { + width: 360px; + margin: 0 20px; + border: 1px solid #ddd; + border-radius: 2px; + padding: 5px; + font-size: 12pt; +} + +.msg-time { + color: rgb(175,175,175); + margin-left: 10px; + font-size: 12px; +} + +.about { padding: 0 40px; padding-top: 10px; } + +.about-line { + display: flex; + flex-direction:column; + margin-top:20px; +} + +.about-label, .about h3 { + color: rgb(152, 171, 181); + color: #999; + margin-bottom:6px; + font-size: 14px; +} + +.about h3 { margin-top:20px; margin-bottom:10px; } +.about img { border-radius:4px; border:1px solid #ddd; } + +.plus { + background-color: rgb(111, 165, 81); + color: white; + width: 16px; + height: 16px; + float: left; + border-radius: 50%; + text-align: center; + font-size: 14px; + font-weight: bold; + margin-right: 5px; + cursor: pointer; +} + +a { + color: rgb(0,158,224); + line-height: 20px; + text-decoration: none; + border-bottom: 1px dashed rgb(0,158,224); +} + +a:hover { + color: rgb(91,89,164); +} + +.more { + padding: 0 40px; +} + +.more h2 { font-weight: normal; font-size: 16pt; margin-top: 30px; margin-bottom: 10px; } + +.more ul { margin: 5px 0; } +.more li { margin-bottom:8px; } + +.button { + background-color: rgb(111, 165, 81); + color: white; + width: 80%; + height: 25px; + border-radius: 10px; + text-align: center; + font-size: 14px; + font-weight: bold; + margin-right: auto; + margin-left: auto; + margin-top: 10px; + padding-top: 4px; + cursor: pointer; +} \ No newline at end of file diff --git a/examples/css/todomvc.css b/examples/css/todomvc.css new file mode 100644 index 000000000..b55906b19 --- /dev/null +++ b/examples/css/todomvc.css @@ -0,0 +1,302 @@ +.program { + margin: 0 auto; + padding: 0; + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #4d4d4d; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; + font-weight: 300; +} + +.todoapp { + flex: 0 0 auto; + min-width: 450px; + max-width: 650px; + height: auto; + margin: 130px auto 40px auto; + + background: #fff; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +.todoapp button, +.todoapp input[type="checkbox"] { + outline: none; +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp h1 { + position: absolute; + top: -155px; + width: 100%; + font-size: 100px; + font-weight: 100; + text-align: center; + color: rgba(175, 47, 47, 0.15); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.todoapp .hidden { + display: none !important; +} + +.todoapp .new-todo, +.todoapp .edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + border: 0; + outline: none; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +.todoapp .new-todo { + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); +} + +.todoapp .toggle-all { + position: absolute; + top: 12px; + left: 14px; + width: 60px; + height: 34px; + text-align: center; + border: none; /* Mobile Safari */ +} + +.todoapp .main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +.todoapp .todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todoapp .todo-list li { + position: relative; + padding-left: 40px; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +.todoapp .todo-list li:last-child { + border-bottom: none; +} + +.todoapp .todo-list li.editing { + display: flex; + flex-direction: row; + border-bottom: none; + padding: 0; + height: 44px; +} + +.todoapp .todo-list li.editing .edit { + display: flex; + flex: 1; + padding: 13px 15px 12px 15px; + margin: -1px 0 0 43px; +} + +.todoapp .todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + left: 0px; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +.todoapp .todo-list li .toggle:after { + content: url('data:image/svg+xml;utf8,'); +} + +.todoapp .todo-list li .toggle:checked:after { + content: url('data:image/svg+xml;utf8,'); +} + +.todoapp .todo-list li label { + white-space: pre-line; + word-break: break-all; + margin-left: 12px; + padding: 8px 6px; + display: block; + line-height: 1.2; + transition: color 0.4s; +} + +.todoapp .todo-list li.completed label { + color: #d9d9d9; + text-decoration: line-through; +} + +.todoapp .todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #cc9a9a; + margin-bottom: 11px; + transition: color 0.2s ease-out; +} + +.todoapp .todo-list li .destroy:hover { + color: #af5b5e; +} + +.todoapp .todo-list li .destroy:after { + position: relative; + content: "×"; + top: 8px; +} + +.todoapp .todo-list li:hover .destroy { + display: block; +} + +.todoapp footer { + color: #777; + padding: 10px 15px; + height: 41px; + text-align: center; + border-top: 1px solid #e6e6e6; +} + +.todoapp footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todoapp .todo-count { + float: left; + text-align: left; +} + +.todoapp .todo-count strong { + font-weight: 300; +} + +.todoapp .clear-completed, +html .todoapp .clear-completed:active { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + cursor: pointer; + position: relative; +} + +.todoapp .clear-completed:hover { + text-decoration: underline; +} + + +.todoapp .filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.todoapp .filters li { + display: inline; +} + +.todoapp .filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.todoapp .filters li a.selected, +.todoapp .filters li a:hover { + border-color: rgba(175, 47, 47, 0.1); +} + +.todoapp .filters li a.selected { + border-color: rgba(175, 47, 47, 0.2); +} diff --git a/examples/editor.eve b/examples/editor.eve new file mode 100644 index 000000000..89d0ab85e --- /dev/null +++ b/examples/editor.eve @@ -0,0 +1,505 @@ +# Editor Bootstrap + +This DB allows Eve programs to interact directly with the editor. + +## Editor Actions + +Actions allow remotely controlling the editor. + +### Inspect + +`#inspect` will enable inspector interactions in the editor. While this is on, additional mouse and cursor events will be attached to the editor to forward the user's current spans / elements of interest to the `@inspector` DB. When the action is removed, inspector mode will be disabled. + +~~~ +search @editor + action = [#inspect] + +bind @inspector + action <- [#inspector] + +bind @browser + action <- [#editor #inspect] +~~~ + +### Jump To + +`#jump-to` will scroll the editor viewport to bring the given `span` or index `position` into view. If multiple `span` and/or `position`s are specified, the topmost will be scrolled to. + +~~~ +search @editor + action = [#jump-to] + (attribute, value) = if action.span then ("span", action.span) + if action.position then ("position", action.position) + +bind @browser + result = [#editor #jump-to] + lookup[record: result, attribute, value] +~~~ + +### Mark Between + +`#mark-between` will create new spans of the specified `type` *between* the given `span`s or `range`s. If `within` (a span id) is specified, it will constrain itself within that span. Otherwise it will go to the beginnning and end of the document. When the action is removed, the associated spans will be removed from the editor. + +~~~ +search @editor + action = [#mark-between type span] + +bind @browser + action <- [#editor #mark-between type span] +~~~ + +~~~ +search @editor + action = [#mark-between type range] + +bind @browser + range <- [start: range.start stop: range.stop] + action <- [#editor #mark-between type range] +~~~ + +Include the optional within attribute, if specified. + +~~~ +search @editor + action = [#mark-between within] + +bind @browser + action <- [within] +~~~ + +### Mark Span + +`#mark-span` will create a new span on the same range as the given `span`s of the specified `type`. When the action is removed, the associated spans will be removed from the editor. + +@NOTE: Mark span must be special cased to copy over all possible optional attributes right now. This is sad. + +~~~ +search @editor + action = [#mark-span type span] + +bind @browser + action <- [#editor #mark-span type span] +~~~ + +Copy optional `message` attribute over if provided. + +~~~ +search @editor + action = [#mark-span message] + +bind @browser + action <- [message] +~~~ + +Copy optional `kind` attribute over if provided. + +~~~ +search @editor + action = [#mark-span kind] + +bind @browser + action <- [kind] +~~~ + +### Mark Range + +`#mark-range` will create a new span using `start` and `stop` as indexes into the text of the current document. When the action is removed, the associated span will be as well. + +~~~ +search @editor + action = [#mark-range type start stop] + +bind @browser + action <- [#editor #mark-range type start stop] +~~~ + +Copy any attributes besides tag over. They'll be used to populate the span's source. + +~~~ +search @editor + action = [#mark-range] + lookup[record: action, attribute, value] + attribute != "tag" + attribute != "type" + attribute != "start" + attribute != "stop" + +bind @browser + lookup[record: action, attribute, value] +~~~ + +### Elide Between Sections + +`#elide-between-section` will elide all sections that do not contain at least one of the given `span`(s) or `position`(s). When the action is removed, these elisions will be cleared. + +~~~ +search @editor + action = [#elide-between-sections span] + +bind @browser + [#editor #elide-between-sections | span] +~~~ + +~~~ +search @editor + action = [#elide-between-sections position] + +bind @browser + [#editor #elide-between-sections | position] +~~~ + +### Find Section + +`#find-section` will find the section (region under the nearest heading) containing the given `position` (as an index into the document text) or `span` id. Results will be returned in the format `[#section position? span? heading start stop]` where `position` and `span` are as provided, heading is the id of the heading span for the section (if any), and `start` and `stop` are the indexes for the beginning and end of the section. + +~~~ +search @editor + action = [#find-section span] + +bind @browser + [#editor #find-section span] +~~~ + +~~~ +search @editor + action = [#find-section position] + +bind @browser + [#editor #find-section position] +~~~ + +~~~ +search @event + event = [#editor #section action] + (attribute, value) = if event.position then ("position", event.position) + if event.span then ("span", event.span) + if event.heading then ("heading", event.heading) + if event.start then ("start", event.start) + if event.stop then ("stop", event.stop) + +commit @editor + section = [#section action] + lookup[record: section, attribute, value] +~~~ + +~~~ +search @editor + section = [#section action] + not(action = [#find-section]) + +commit @editor + section := none +~~~ + +## Language Service + +The Language Service allows Eve to query the analyzer for information about the currently running program. + +### Find Source + +`#find-source` will request the originating source span(s) for a given `record` id, `record`, `attribute` pair, or `span` ids. from the language service. Results will be returned in the format `[#source record? attribute? span? block]`, where `record`, `attribute` are the same as provided. `span` is the same if provided or originating spans if not, and block is the set of source blocks. These records will be maintained by the editor until the action is removed, at which point they will be removed from the DB. + +~~~ +search @editor + action = [#find-source] + (attribute, value) = if action.record then ("record", action.record) + if action.attribute then ("attribute", action.attribute) + if action.span then ("span", action.span) + +bind @browser + action = [#editor #find-source] + lookup[record: action, attribute, value] +~~~ + +When a source event comes in, persist it into the editor and clear it from the event bag. + +~~~ +search @event + event = [#editor #source action] + (attribute, value) = if event.record then ("record", event.record) + if event.attribute then ("attribute", event.attribute) + if event.span then ("span", event.span) + if event.block then ("block", event.block) + +commit @editor + source = [#source action] + lookup[record: source, attribute, value] + +commit @event + event := none +~~~ + +When a source's supporting action is removed, remove it. +~~~ +search @editor + source = [#source action] + not(action = [#find-source]) + +commit @editor + source := none +~~~ + + +### Find Related + +`#find-related` will request all related `span`(s) for a given `variable` or `variable`(s) for a `span` from the language service. Related spans are spans which are directly joined to or unified with the target. Results will be returned in the format `[#related span? variable?]`, where the argument you provided is the same and the other is filled in. These records will be maintained by the editor until the action is removed, at which point they will be removed from the DB. + +~~~ +search @editor + [#find-related variable] + +bind @browser + [#editor #find-related | variable] +~~~ + +~~~ +search @editor + [#find-related span] + +bind @browser + [#editor #find-related | span] +~~~ + +When a related event comes in, persist it into the editor and clear it from the event bag. + +~~~ +search @event + event = [#editor #related action variable span] + +commit @editor + event <- [#related action variable span] + +commit @event + event := none +~~~ + +When a related's supporting action is removed, remove it. + +~~~ +search @editor @browser + related = [#related action] + not(action = [#find-related]) + +commit @editor + related := none +~~~ + +### Find Value + +`#find-value` will request the intermediate value(s) for the given set of `variable`(s) from the language service. By default, all intermediates for the variables will be returned. If `given` is specified, containing a set of `[attribute value]` pairs, only values that exist in the same "row" as the fixed values will be returned. If multiple values are provided for a variable, the results for each will be logically unioned. Results will be returned in the format `[#value variable value row]`, where `variable` is one of the given `variable`s, `value` is the intermediate value of the variable, and `row` is the "row" the intermediate is from (for matching intermediates of different variables up). These records will be maintained by the editor until the action is removed, at which point they will be removed from the DB. + +If given isn't specified, we'll find up to 100 rows out of all the rows from the block. + +~~~ +search @editor + action = [#find-value variable not(given)] + +bind @browser + action <- [#editor #find-value variable] +~~~ + +If it is, we'll filter by it to only include relevant rows. + +~~~ +search @editor + action = [#find-value variable given: [attribute value]] + +bind @browser + given = [attribute value] + action <- [#editor #find-value variable given] +~~~ + +When a value event comes in, persist it into the editor and clear it from the event bag. + +~~~ +search @event + event = [#editor #value action variable value row name register] + +commit @editor + event <- [#value action variable value row name register] + +commit @event + event := none +~~~ + +When a value's supporting action is removed, remove it. + +~~~ +search @editor @browser + value = [#value action] + not(action = [#find-value]) + +commit @editor + value := none +~~~ + + +### Find Cardinality + +`#find-cardinality` will request the cardinalities for the given set of `variable`(s) from the language service. The cardinality is the number of unique values that could satisfy `variable`. Results will be returned in the format `[#cardinality variable cardinality]` where `variable` is as given and cardinality is the count of satisfying values available. These records will be maintained by the editor until the action is removed, at which point they will be removed from the DB. + +~~~ +search @editor + action = [#find-cardinality variable] + +bind @browser + [#editor #find-cardinality | variable] +~~~ + +When a value event comes in, persist it into the editor and clear it from the event bag. + +~~~ +search @event + event = [#editor #cardinality action variable cardinality] + +commit @editor + event <- [#cardinality action variable cardinality] + +commit @event + event := none +~~~ + +When a value's supporting action is removed, remove it. + +~~~ +search @editor @browser + cardinality = [#cardinality action] + not(action = [#find-cardinality]) + +commit @editor + cardinality := none +~~~ + +### Find Affector + +~~~ +search @editor + action = [#find-affector] + (attribute, value) = if action.record then ("record", action.record) + if action.attribute then ("attribute", action.attribute) + if action.span then ("span", action.span) + +bind @browser + result = [#editor #find-affector] + lookup[record: result, attribute value] +~~~ + +When an affector event comes in, persist it into the editor and clear it from the event bag. + +~~~ +search @event + event = [#editor #affector action] + (attribute, value) = if event.record then ("record", event.record) + if event.attribute then ("attribute", event.attribute) + if event.span then ("span", event.span) + if event.block then ("block", event.block) + if event.action then ("action", event.action) + +commit @editor + event <- [#affector action] + lookup[record: event, attribute, value] + +commit @event + event := none +~~~ + +When an affector's supporting action is removed, remove it. + +~~~ +search @editor @browser + affector = [#affector action] + not(action = [#find-affector]) + +commit @editor + affector := none +~~~ + +### Find Failure + +~~~ +search @editor + action = [#find-failure block] + +bind @browser + [#editor #find-failure | block] +~~~ + +~~~ +search @event + event = [#editor #failure action block start stop] + +commit @editor + event <- [#failure action block start stop] + +commit @event + event := none +~~~ + +~~~ +search @editor @browser + failure = [#failure action] + not(action = [#find-failure]) + +commit @editor + failure := none +~~~ + +### Find Root Drawers + +~~~ +search @editor + action = [#find-root-drawers] + +bind @browser + action <- [#editor #find-root-drawers] +~~~ + +~~~ +search @event + event = [#editor #root-drawer action span start stop] + +commit @editor + event <- [#root-drawer action span start stop] + +commit @event + event := none +~~~ + +~~~ +search @editor @browser + drawer = [#drawer action] + not(action = [#find-root-drawer]) + +commit @editor + drawer := none +~~~ + +### Find Performance + +~~~ +search @editor + action = [#find-performance] + +bind @browser + action <- [#editor #find-performance] +~~~ + +~~~ +search @event + event = [#editor #performance action block average calls color max min percent total] + +commit @editor + event <- [#performance action block average calls color max min percent total] + +commit @event + event := none +~~~ + +~~~ +search @editor @browser + performance = [#performance action] + not(action = [#find-performance]) + +commit @editor + performance := none +~~~ diff --git a/examples/event.eve b/examples/event.eve new file mode 100644 index 000000000..5bdebb304 --- /dev/null +++ b/examples/event.eve @@ -0,0 +1,53 @@ +remove events + +~~~ +search @event + c = if event = [#keydown] then event + if event = [#keyup] then event + if event = [#click] then event + if event = [#double-click] then event + if event = [#change] then event + if event = [#blur] then event + if event = [#focus] then event + if event = [#url-change] then event +commit @event + c := none +~~~ + +hash changes + +~~~ +search @event + change = [#url-change hash-segment] + hash-segment = [index value] +commit @browser + url = [#url] + url.hash-segment := [index value] +~~~ + +update values + +~~~ +search @event + c = [#change element value] +commit @browser + element.value := value +~~~ + +add checked + +~~~ +search @event + c = [#change element checked: true] +commit @browser + element.checked := true +~~~ + +remove checked + +~~~ +search @event + c = [#change element checked: false] +commit @browser + element.checked := none +~~~ diff --git a/examples/flappy.eve b/examples/flappy.eve new file mode 100644 index 000000000..45bea4732 --- /dev/null +++ b/examples/flappy.eve @@ -0,0 +1,236 @@ +# Flappy Eve + +When a player starts the game, we commit a `#world`, a `#player`, and some `#obstacles`. These will keep all of the essential state of the game. All of this information could have been stored on the world, but for clarity we break the important bits of state into objects that they effect. + +- The `#world` tracks the distance the player has travelled, the current game screen, and the high score. +- The `#player` stores his current y position and (vertical) velocity. +- The `obstacles` have their (horizontal) offset and gap widths. We put distance on the world and only keep two obstacles; rather than moving the player through the world, we keep the player stationary and move the world past the player. When an obstacle goes off screen, we will wrap it around, update the placement of its gap, and continue on. + +## Setup + +Add a flappy eve and a world for it to flap in: + +~~~ +commit + [#player #self name: "eve" x: 25 y: 50 velocity: 0] + [#world screen: "menu" frame: 0 distance: 0 best: 0 gravity: -0.061] + [#obstacle gap: 35 offset: 0] + [#obstacle gap: 35 offset: -1] +~~~ + +Next we draw the backdrop of the world. The player and obstacle will be drawn later based on their current state. Throughout the app we use resources from [@bhauman's flappy bird demo in clojure][1]. Since none of these things change over time, we commit them once when the player starts the game. + +### Draw the game world! + +~~~ +search + world = [#world] + +commit @browser + world <- [#div style: [user-select: "none" -webkit-user-select: "none" -moz-user-select: "none"] children: + [#svg #game-window viewBox: "10 0 80 100", width: 480 children: + [#rect x: 0 y: 0 width: 100 height: 53 fill: "rgb(112, 197, 206)" sort: 0] + [#image x: 0 y: 52 width: 100 height: 43 preserveAspectRatio: "xMinYMin slice" href: "https://cdn.rawgit.com/bhauman/flappy-bird-demo/master/resources/public/imgs/background.png" sort: 1] + [#rect x: 0 y: 95 width: 100 height: 5 fill: "rgb(222, 216, 149)" sort: 0]]] +~~~ + +## Game menus + +These following blocks handle drawing the game's other screens (such as the main menu and the game over scene). + +The main menu displays a message instructing the player how to start the game. + +~~~ +search @browser @session + [#world screen: "menu"] + svg = [#game-window] + +bind @browser + svg.children += [#text x: 50 y: 45 text-anchor: "middle" font-size: 6 text: "Click the screen to begin!" sort: 10] +~~~ + +The "game over" screen displays the final score of the last game, the high score of all games, and a message inviting the player to play the game again. + +~~~ +search @session @browser + [#world screen: "game over" score best] + svg = [#game-window] + +bind @browser + svg.children += [#text x: 50 y: 30 text-anchor: "middle" font-size: 6 text: "Game Over :(" sort: 10] + svg.children += [#text x: 50 y: 55 text-anchor: "middle" font-size: 6 text: "Score {{score}}" sort: 10] + svg.children += [#text x: 50 y: 65 text-anchor: "middle" font-size: 6 text: "Best {{best}}" sort: 10] + svg.children += [#text x: 50 y: 85 text-anchor: "middle" font-size: 4 text: "Click to play again!" sort: 10] +~~~ + +### Score calculation + +We haven't calculated the score yet, so let's do that. We calculate the score as the `floor` of the distance, meaning we just round the distance down to the nearest integer. If the distance between pipes is changed, this value can be scaled to search. + +~~~ +search + world = [#world distance] + +bind + world.score := floor[value: distance] +~~~ + +### Start a new game + +When the game is on the "menu" or "game over" screens, a click anywhere in the application will (re)start the game. Additionally, if the current score is better than the current best, we'll swap them out now. Along with starting the game, we make sure to reset the distance and player positions in the came of a restart. + +~~~ +search @event @session + [#click element: [#world]] + world = if world = [#world screen: "menu"] then world + else [#world screen: "game over"] + new = if world = [#world score best] score > best then score + else if world = [#world best] then best + player = [#player] + +commit + world <- [screen: "game" distance: 0 best: new] + player <- [x: 25 y: 50 velocity: 0] +~~~ + +## Drawing + +### Player + +Next we draw the `#player` at its (x,y) coordinates. Since the player is stationary in x, setting his x position here dynamically is just a formality, but it allows us to configure his position on the screen when we initialize. We create the sprite first, then set the x and y positions to let us reuse the same element regardless of where the player is. + +Draw the player + +~~~ +search @session @browser + svg = [#game-window] + player = [#player x y] + +bind @browser + sprite = [#image player | width: 10 height: 10 href: "http://i.imgur.com/sp68LtM.gif" sort: 8] + sprite.x := x - 5 + sprite.y := y - 5 + svg.children += sprite +~~~ + +### Obstacles + +Drawing obstacles is much the same process as drawing the player, but we encapsulate the sprites into a nested SVG to group and move them as a unit. + +Draw the obstacles + +~~~ +search @session @browser + svg = [#game-window] + obstacle = [#obstacle x height gap] + bottom-height = height + gap + imgs = "https://cdn.rawgit.com/bhauman/flappy-bird-demo/master/resources/public/imgs" + +bind @browser + sprite-group = [#svg #obs-spr obstacle sort: 2 overflow: "visible" children: + [#image y: 0 width: 10 height, preserveAspectRatio: "none" href: "{{imgs}}/pillar-bkg.png" sort: 1] + [#image x: -1 y: height - 5 width: 12 height: 5 href: "{{imgs}}/lower-pillar-head.png" sort: 2] + [#image y: bottom-height width: 10 height: 90 - bottom-height, preserveAspectRatio: "none" href: "{{imgs}}/pillar-bkg.png" sort: 1] + [#image x: -1 y: bottom-height width: 12 height: 5 href: "{{imgs}}/lower-pillar-head.png" sort: 2]] + sprite-group.x := x + svg.children += sprite-group +~~~ + +## Game Logic + +Now we need some logic to actually play the game. We slide obstacles along proportional to the distance travelled, and wrap them around to the beginning once they're entirely off screen. Additionally, we only show obstacles once their distance travelled is positive. This allows us to offset a pipe in the future, without the modulo operator wrapping it around to start off halfway through the screen. + +### Obstacles + +Every 2 distance a wild obstacle appears + +~~~ +search + [#world distance] + obstacle = [#obstacle offset] + obstacle-distance = distance + offset + obstacle-distance >= 0 + +bind + obstacle <- [x: 100 - (50 * mod[value: obstacle-distance, by: 2])] +~~~ + +When the obstacle is offscreen (`x > 90`), we randomly adjust the height of its gap to ensure the game doesn't play the same way twice. Eve's current random implementation yields a single result per seed per evaluation, so you can ask for `random[seed: "foo"]` in multiple queries and get the same result in that evaluation. In practice, this means that for every unique sample of randomness you care about in a program at a fixed time, you should use a unique seed. In this case, since we want one sample per obstacle, we just use the obstacle UUIDs as our seeds. The magic numbers in the equation just keep the gap from being at the very top of the screen or underground. + +Readjust the height of the gap every time the obstacle resets + +~~~ +search + [#world screen: "game" frame] + obstacle = [#obstacle x > 90] + height = random[seed: frame] * 30 + 5 + +commit + obstacle.height := height +~~~ + +### Flapping the player + +When a player clicks during gameplay, we give the bird some lift by setting its velocity. + +~~~ +search @event @session + [#click element: [#world]] + [#world screen: "game"] + player = [#player #self] + +commit + player.velocity := 1.17 +~~~ + +### Scroll the world + +Next, we scroll the world in time with frame updates. Eve is currently locked to 60fps updates here, but this will probably be configurable in the future. Importantly, we only want to update the world state once per frame, so to ensure that we note the offset of the frame we last computed in `world.frame` and ensure we’re not recomputing for the same offset. + +~~~ +search @session @event + [#time frames] + world = [#world screen: "game" frame != frames gravity] + player = [#player y velocity] + not([#click]) + +commit + world.frame := frames + world.distance := world.distance + 1 / 60 + player <- [y: y - velocity, velocity: velocity + gravity] +~~~ + +### Collision + +Checking collision with the ground is very simple. Since we know the y height of the ground, we just check if the player's bottom (determined by center + radius) is below that point. + +The game is lost if the player hits the ground. + +~~~ +search + world = [#world screen: "game"] + [#player y > 85] // ground height + player radius + +commit + world.screen := "game over" +~~~ + +Collision with the pipes is only slightly harder. Since they come in pairs, we first determine if the player is horizontally in a slice that may contain pipes and if so, whether we're above or below the gap. If neither, we're in the clear, otherwise we've collided. + +The game is lost if the player hits an `#obstacle` + +~~~ +search + world = [#world screen: "game"] + [#player x y] + [#obstacle x: obstacle-x height gap] + ∂x = abs[value: obstacle-x + 5 - x] - 10 // distance between the edges of player and obstacle (offset of 1/2 obstacle width because origin is on the left) + ∂x < 0 + collision = if y - 5 <= height then true + else if y + 5 >= gap + height then true + +commit + world.screen := "game over" +~~~ + +[1]: https://github.com/bhauman/flappy-bird-demo diff --git a/examples/form-binding.eve b/examples/form-binding.eve new file mode 100644 index 000000000..534c76064 --- /dev/null +++ b/examples/form-binding.eve @@ -0,0 +1,69 @@ +# Working With Forms + +## Inputs + +Display a textbox + +``` +commit @browser + [#label text: "Enter your name: " children: [#input type: "text" name: "name" value: "Bill"]] +``` + +Get the value of the textbox and display it in a message + +``` +search @browser + [#input name: "name" value] + +bind @browser + [#div text: "Hi there {{value}}."] +``` + +## Radio buttons + +Display radio buttons + +``` +commit @browser + [#h3 text: "What's your favorite flavor of ice cream?"] + [#label text: "Vanilla" children: [#input type: "radio" name: "flavor" value: "vanilla"]] + [#label text: "Chocolate" children: [#input type: "radio" name: "flavor" value: "chocolate"]] + [#label text: "Matcha" children: [#input type: "radio" name: "flavor" value: "matcha"]] +``` + +All flavors except Matcha are a good choice + +``` +search @browser + [#input name: "flavor" checked value] + text = if value != "matcha" then "Good choice!" + else "To each their own I guess." + +bind @browser + [#div text: "Good choice!"] +``` + +## Checkboxes + +Display checkboxes + +``` +commit @browser + [#h3 text: "What's would you like to eat?"] + [#label text: "Rice" children: [#input type: "checkbox" name: "food" value: "rice"]] + [#label text: "Meat" children: [#input type: "checkbox" name: "food" value: "meat"]] + [#label text: "Pickles" children: [#input type: "checkbox" name: "food" value: "pickles"]] + [#label text: "Soup" children: [#input type: "checkbox" name: "food" value: "soup"]] +``` + +Search for inputs named "food" that are checked, and bind them display them in a list + +``` +search @browser + [#input name: "food" value checked] + +bind @browser + [#div text: "There are some bowls here containing:" children: + [#ul children: + [#li text: value]]] +``` \ No newline at end of file diff --git a/examples/gaussian.eve b/examples/gaussian.eve new file mode 100644 index 000000000..48f8f8608 --- /dev/null +++ b/examples/gaussian.eve @@ -0,0 +1,64 @@ +# Plot a Gaussian Distribution + +This program demonstrates event injection and drawing with SVGs. + +Create an svg to draw into. + +~~~ +bind @browser + [#svg viewBox: "0 0 50 2", width: 200, height: 200] +~~~ + +Rescale the svg area based on the maximum value in a histogram slot. Most browsers seem to also use this to rescale x to keep the aspect ratio constant. + +~~~ +search + [#slots x total] + m = max[value: total, given: total] + +search @browser + s = [#svg] + +bind @browser + s.viewBox := "0 0 50 {{m + 2}}" +~~~ + +For each click, generate a gaussian sample, use floor to bin it into histogram by integer. + +~~~ +search @event @session + e = [#click] + x = floor[value: gaussian[seed: e, σ: 10, μ: 25]] + total = if [#slots x total] then total + 1 else 1 + +commit + s = [#slots x] + s.total := total +~~~ + +Draw the bins as black rectangles. + +~~~ +search + [#slots x total] + m = max[value: total, given: total] + +search @browser + s = [#svg] + +commit @browser + k = [#rect x width: 1, stroke-width: 1, stroke: "black"] + k.y := m - total + k.height := total + s.children += k +~~~ + +Inject a click once for each 1/60s. + +~~~ +search + [#time frames] + +bind @event + [#click frames] +~~~ diff --git a/examples/hello-world.eve b/examples/hello-world.eve new file mode 100644 index 000000000..df59d5779 --- /dev/null +++ b/examples/hello-world.eve @@ -0,0 +1,116 @@ +# Hello World +Hi guys, this is my first Eve program, just an hello world! Let's code! +```eve disabled +bind @browser +[tag: "div", text: "hello world!"] +``` + +Now, let's search for some records, let's say those with an attribute dude: +```eve disabled +search + [dude] + +bind @browser + [tag: "div", text: "hello world!"] +``` + +So, actually there's no record with an attribute "dude", let's add one to the document: +```eve disabled +commit + [dude: "wat"] +``` + +Ok, let's embed some attribute with the string operator: +```eve disabled +search + [dude] + +bind @browser + [#div text: "hello {{dude}}"] +``` + +Now, let's search for tag and attributes: +```eve disabled +search + [#student name grade school] + +bind @browser + [#div text: "I'm a student called {{name}}, in {{school}} and I've {{grade}}"] +``` + +Ok, let's add a student: +```eve disabled +search + wilk = [name: "Wilk"] + +bind + wilk <- [#student grade: 4, school: "a school", age: 29] +``` + +## wat +let's make this real +### section below +that's much better +# Test +Records students: +```eve disabled +commit @session + [#student name: "Wilk", grade: 20, age: 29] + [#student name: "Kya", grade: 30, age: 28] +``` + +Increment the age of Kya: +```eve disabled +search @session + student = [#student name: "Kya"] + +bind @session + student.age += 1 +``` + +## Views +Define a button to load students: +``` +bind @browser + [#button text: "load students"] + +``` + +Display the students list: +``` +search @session + student = [#student] + +bind @browser + [#h1 text: "Students List"] + [#div class: "card is-fullwidth", children: + [#div class: "card-header", children: + [#p class: "card-header-title", text: "Student {{student.name}}"] + ] + [#div class: "card-content", children: + [#div class: "content", text: "{{student.name}} it's {{student.age}} and has {{student.grade}}"] + ] + ] +``` + +## Application Logic +Get students from the previous call: +``` +search @http + [#request #students-list response: [json]] + json = [#array] + lookup[record: json, attribute, value: [id name grade age]] + +commit @session + student = [#student id name grade age] +``` + +## Events +Request students from a REST service: +``` +search @event @browser @session + [#click element: [#button]] + +commit @http + [#request #students-list url: "http://localhost:12345/students"] +``` diff --git a/examples/http.eve b/examples/http.eve new file mode 100644 index 000000000..4ad14f6ae --- /dev/null +++ b/examples/http.eve @@ -0,0 +1,68 @@ +# HTTP Requests + +This is a simple example that sends an HTTP request to a website. The website returns a JSON result contains a list of users and some info about them. We parse those results and format them for display. + +Draw a button, that when clicked will send the HTTP request + +~~~ +bind @browser + [#button style: [height: 40], text: "Send an HTTP Request"] +~~~ + +## Send a request + +On click, send the HTTP request. We're sending it to a service that sends back dummy data in a JSON format. We're expecting a list of users and some data about them. + +~~~ +search @event @browser @session + [#click element: [#button]] + +commit @http + [#request #test-data url: "https://jsonplaceholder.typicode.com/users/"] +~~~ + +Display a message while the request is awaiting a response. If you're on a fast connection, you probably won't even see this message. Try disabling your wifi and sending a request, and you'll see this message displayed. + +~~~ +search @http + r = [#request not(response)] + total = count[given: r] + +bind @browser + [#div text: "Requests in flight: {{total}}"] +~~~ + +## Parse a response + +We get back a list of users with the following JSON structure: + +`{ id, username, email, address: { street, suite, city, zipcode, geo: { lat, lng }}, phone, website, company: { name, catchPhrase, bs }}` + +We're only going to display a couple of the attributes, so we can take only the ones we care about. Order doesn't matter + +~~~ +search @http + [#request #test-data response: [json]] + json = [#array] + lookup[record: json, attribute, value: [id name email phone website address: [street suite city zipcode]]] + +commit + [#user id name email phone website address: [street suite city zip: zipcode]] +~~~ + +Display the responses in a formatted list. + +~~~ +search + [#user id name email address phone website] + +bind @browser + [#div text: "Results: {{count[given: id]}}"] + [#div sort: id, children: + [#h3 text: "{{id}}: {{name}}"] + [#div text: "e-mail: {{email}}"] + [#div text: "phone: {{phone}}"] + [#div text: "website:" children: + [#a href: website, text: website]] + [#div text: "address: {{address.street}}, {{address.city}} {{address.zip}}"]] +~~~ diff --git a/examples/inspector.eve b/examples/inspector.eve new file mode 100644 index 000000000..a66d43d82 --- /dev/null +++ b/examples/inspector.eve @@ -0,0 +1,946 @@ +# Inspector + +The inspector ("don't panic" button) is Eve's interactive debugging tool. It acts as a go between to help the Analyzer understand what's wrong based on your input. It handles the subset of issues that Eve can't know about a priori, where the program is valid and reasonable but not behaving as you intend; e.g.: + +- *"I shouldn't be able to see this button when I'm not logged in."* +- *"The student attendance list is missing."* +- *"Too many emails were sent to each client."* +- *"The clock hand in the wrong position."* +- *"This program slow."* + +Once Eve understands what the issue is, it provides a set of structured questions to work backwards through the symptoms to the source. Just telling you "the block labelled 'draw a sign out button when logged in' drew the sign out button" is worse than useless. However, with a little help from you ("that user shouldn't have the tag `#logged-in` right now), Eve can work backwards and say "the `#logged-in` tag was never removed from the user because she did not have an email attribute. + +When you've arrived at the root of the problem, the inspector provides tools to figure out why it happened. If, instead a missing attribute, the issue involved incorrect values (perhaps the result of mathematical error), Eve can show you intermediates to find where the breakdown occurs. When too many or too few rows match a pattern, Eve can show you the cardinality of the actions in a block to get a sense of why the numbers don't add up. Some issues the inspector can help diagnose can be found in the **Example Scenarios** section. + + +## Events + +Clean the events we care about up. + +~~~ +search @event + event = if e = [#inspector #inspect] then e + if e = [#inspector #clear] then e + +commit @event + event := none +~~~ + +### Inspect + +When the inspect event comes in, open a new inspector targeted to the inspected element/record/span. + +If an inspector is already open, close it and nuke its state. + +~~~ +search @event + [#inspector #inspect] + +search @inspector + inspector = [#inspector] + +bind @event + [#inspector #clear inspector] +~~~ + +When an element is targeted, inspect it. + +~~~ +search @event + event = [#inspector #inspect #direct-target target: element type: "element" x y] + + +commit @inspector + [#inspector element state: ("popout", "annotate-blocks") | x y] + +commit @event + event := none +~~~ + +When the document root is targeted, inspect it. + +~~~ +search @event + event = [#inspector #inspect #direct-target type: "root" x y] + + +commit @inspector + [#inspector root: "true" state: "popout" | x y] + +commit @event + event := none +~~~ + +When a block is targeted, inspect it. + +~~~ +search @event + event = [#inspector #inspect target: block type: "code_block" x y] + +commit @inspector + [#inspector block state: ("block-popout") | x y] + +commit @event + event := none +~~~ + +### Focus/Unfocus Current + +When inspecting, the navigator will reveal a button which can be clicked to elide everything but the annotated sections of the document. + +~~~ +search @event + event = [#inspector #focus-current] + +search @inspector + inspector = [#inspector state] + focus-states = if state = "annotate-blocks" then "focus-blocks" + if state = "annotate-affectors" then "focus-affectors" + if state = "annotate-failures" then "focus-failures" + if state = "annotate-performance" then "focus-performance" + +commit @inspector + inspector.state += focus-states + +commit @event + event := none +~~~ + +~~~ +search @event + event = [#inspector #unfocus-current] + +search @inspector + inspector = [#inspector state] + focus-states = if state = "annotate-blocks" then "focus-blocks" + if state = "annotate-affectors" then "focus-affectors" + if state = "annotate-failures" then "focus-failures" + if state = "annotate-performance" then "focus-performance" + +commit @inspector + inspector.state -= focus-states + +commit @event + event := none +~~~ + +### Clear + +When an inspector clear event comes in, remove open inspectors and all their state. + +~~~ +search @event @inspector + event = [#inspector #clear] + inspector = if event.inspector then event.inspector + else [#inspector] + +commit @inspector + inspector := none + inspector.state := none + inspector.data := none +~~~ + +### Click Option + +When an inspector button is pressed, activate any new states we want to transition into. + +~~~ +search @event + [#click #direct-target element] + +search @browser + element = [#button inspector activate] + +commit @inspector + inspector.state += activate +~~~ + +When an inspector button is pressed, deactivate any old states we may have been in that want to leave. + +~~~ +search @event + [#click #direct-target element] + +search @browser + element = [#button inspector deactivate] + +commit @inspector + inspector.state -= deactivate +~~~ + +### Click Attribute Table + +When a row in the attribute table is clicked, find the source of that particular attribute. + +~~~ +search @event + [#click element] + // If we're clicking on an entity value, we'll handle that elsewhere + [#click element: value-elem] + +search @browser + not(value-elem = [#value is-entity: true]) + + element = [#kv-row key] + [#kv-table inspector children: element] + +commit @inspector + inspector.attribute := key +~~~ + +When we click a value in the attribute table, if it's an entity navigate to it. + +~~~ +search @event + [#click element] + [#click element: value-elem] + +search @browser + value-elem = [#value is-entity: true text: entity] + + element = [#kv-row key] + [#kv-table inspector children: element] + +search @inspector + x = inspector.x + y = inspector.y + +bind @event + [#inspector #inspect #direct-target target: entity type: "element" x y] +~~~ + +### Click Elsewhere + +Any click that is entirely outside of the inspector will close it. + +~~~ +search @inspector + inspector = [#inspector] + +search @event + event = [#click] + not([#click element: inspector]) + +commit @inspector + inspector := none +~~~ + +While the inspector is open on an element, update its avs to that element's avs. This will persist until it's no longer potentially relevant. +@NOTE: This is kind of weird and it may be better to have multiple targets instead. + +~~~ +search @inspector + inspector = [#inspector element] + +search @browser + lookup[record: element, attribute, value] + +commit @inspector + inspector.avs := [attribute value] +~~~ + +## Data + +As the inspector states change, it will need to gather different data from the editor and language service. Data requests will be added to the `data` attribute and requests for that data will be bound for so long as those values exist. By gathering all the data in a separate pass, UI to display it can share the same data and be written immediate-mode style. + +Certain kinds of data depend on other data. + +~~~ +commit @inspector + [#data data: "sources"] + [#data data: "source-sections", requires: "sources"] + [#data data: "related" requires: "sources"] + [#data data: "cardinalities" requires: "related"] + [#data data: "values" requires: "related"] + [#data data: "all-values" requires: "related"] + [#data data: "affectors"] + [#data data: "affector-failures" requires: "affectors"] + [#data data: "performance-stats"] +~~~ + +If we need data that has a dependency, make sure we're fetching that too. + +~~~ +search @inspector + inspector = [#inspector data] + [#data data requires] + +bind @inspector + inspector.data += requires +~~~ + +### Sources + +Find source(s) for the current record. + +~~~ +search @inspector + [#inspector data: "sources" element: record not(attribute)] + +bind @editor + [#find-source record] +~~~ + +Find source(s) for a specific attribute of the current record. + +~~~ +search @inspector + [#inspector data: "sources" element: record attribute] + +bind @editor + [#find-source record attribute] +~~~ + +### Source Sections + +Find the section(s) containing the found source(s). + +~~~ +search @inspector + [#inspector data: "source-sections" element: record] + +search @editor + [#source record block] + +bind @editor + [#find-section span: block] +~~~ + +### Related + +Find related(s) for the current record's source action(s). + +~~~ +search @inspector + inspector = [#inspector data: "related" element: record] + +search @editor + [#source record span] + +bind @editor + [#find-related inspector | span] +~~~ + +### Values + +Find the intermediate values that contribute to the currently targeted record. + +~~~ +search @inspector + inspector = [#inspector data: "values" element avs: [attribute value]] + +search @editor + [#source record: element block span] + [#related span variable] + +bind @editor + [#find-value inspector | variable given: [attribute value]] +~~~ + +### All Values + +Find all intermediate values for the current target's related variable(s). + +~~~ +search @inspector + (inspector, element) = if i = [#inspector data: "all-values" element] then (i, element) + else if i = [#inspector data: "values" element not(avs)] then (i, element) + +search @editor + [#source record: element block span] + [#related span variable] + +bind @editor + [#find-value inspector | variable] +~~~ + +### Cardinalities + +Find cardinalities for the current target's related variable(s). + +~~~ +search @inspector + inspector = [#inspector data: "cardinalities" element] + +search @editor + [#source record: element block span] + [#related span variable] + +bind @editor + [#find-cardinality inspector | variable] +~~~ + +### Affectors + +Find the set of blocks that could affect the currently targeted element. + +~~~ +search @inspector + inspector = [#inspector data: "affectors" element] + +bind @editor + [#find-affector record: element attribute: "children"] +~~~ + +Find the set of blocks that could draw into the document root + +~~~ +search @inspector + inspector = [#inspector data: "affectors" root] + +bind @editor + [#find-root-drawers] +~~~ + +### Affector Failures + +Find the reasons why affectors of the current target failed. + +~~~ +search @inspector + inspector = [#inspector data: "affector-failures" element] + +search @editor + [#affector record: element block] + +bind @editor + [#find-failure block] +~~~ + +### Block Failure + +Find the reason why the targeted block failed. + +~~~ +search @inspector + inspector = [#inspector data: "block-failure" block] + +bind @editor + [#find-failure block] +~~~ + +### Performance Stats + +~~~ +search @inspector + inspector = [#inspector data: "performance-stats"] + +bind @editor + [#find-performance] +~~~ + +## States + +Inspector states control how the inspector interacts with the editor and presents itself to the user. + +Certain states require data to be fetched. + +~~~ +commit @inspector + // Element States + [#state state: "focus-blocks" requires: "sources"] + [#state state: "annotate-blocks" requires: "sources"] + [#state state: "highlight-related" requires: "related"] + [#state state: "show-values-inline" requires: "values"] + [#state state: "show-values-table" requires: "values"] + [#state state: "show-cardinalities-inline" requires: "cardinalities"] + [#state state: "focus-affectors" requires: "affectors"] + [#state state: "annotate-affectors" requires: "affectors"] + [#state state: "focus-failures" requires: "affector-failures"] + [#state state: "annotate-failures" requires: "affector-failures"] + [#state state: "highlight-failures" requires: "affector-failures"] + + // Block States + [#state state: "highlight-block-failure" requires: "block-failure"] + [#state state: "annotate-performance" requires: "performance-stats"] + [#state state: "focus-performance" requires: "performance-stats"] +~~~ + +When the inspector is in a state that requires data, request that data. + +~~~ +search @inspector + [#state state requires] + inspector = [#inspector state] + +bind @inspector + inspector.data += requires +~~~ + +### Focus Blocks + +When the `"focus-blocks"` state is active, elide everything but the originating blocks for the current target. + +~~~ +search @inspector + inspector = [#inspector state: "focus-blocks" element] + +search @editor + [#source record: element block] + +bind @editor + [#elide-between-sections inspector | span: block] + [#jump-to position: 0] +~~~ + +### Annotate Blocks + +When the `"annotate-blocks"` state is active, annotate each source block for the current target. + +~~~ +search @inspector + inspector = [#inspector state: "annotate-blocks" element] + +search @editor + [#source record: element block] + +bind @editor + [#mark-span inspector type: "block_annotation" span: block kind: "source" message: "This block provides the inspected value."] + [#jump-to span: block] +~~~ + +### Highlight Related + +When the `"highlight-related"` state is active, mark all related spans highlighted. + +~~~ +search @inspector + inspector = [#inspector state: "highlight-related" element] + +search @editor + [#source record: element span] + [#related span variable] + +bind @editor + [#mark-span inspector type: "highlight" | span: variable] +~~~ + +### Show Values Inline + +When the `"show-values-inline"` state is active, badge any found intermediate values. + +~~~ +search @inspector + inspector = [#inspector state: "show-values-inline" element] + +search @editor + [#source record: element block span] + [#related span variable] + [#value variable value row: 1] + +bind @editor + [#mark-span inspector type: "badge" kind: "intermediate" target: element message: value span: variable] +~~~ + +### Show Values Table + +When the `"show-values-table"` state is active, embed a table of found intermediate values. + +~~~ +search @inspector + inspector = [#inspector state: "show-values-table" element] + +search @editor + [#source record: element block span] + [#related span variable] + [#value variable value row name] + +bind @view + row-guy = [row] + lookup[record: row-guy attribute: name value] + table = [#table inspector | field: name row: row-guy] + +bind @browser + table.span := block +~~~ + +### Show Cardinalities Inline + +When the `"show-cardinalities-inline"` state is active, badge any found cardinalities. + +~~~ +search @inspector + inspector = [#inspector state: "show-cardinalities-inline" element] + +search @editor + [#source record: element block span] + [#related span variable] + [#cardinality variable cardinality] + +bind @editor + [#mark-span inspector type: "badge" kind: "cardinality" target: element message: cardinality | span: variable] +~~~ + +### Focus Affectors + +When the `"focus-affectors"` state is active, elide everything but the blocks which could impact the target. + +~~~ +search @inspector + inspector = [#inspector state: "focus-affectors" element] + +search @editor + [#affector record: element block action] + +bind @editor + [#elide-between-sections inspector | span: block] + [#jump-to position: 0] + [#mark-span inspector type: "highlight" | span: action] +~~~ + +### Annotate Affectors + +When the `"annotate-affectors"` state is active, annotate blocks which could impact the target element. + +~~~ +search @inspector + inspector = [#inspector state: "annotate-affectors" element] + +search @editor + [#affector record: element block action] + +bind @editor + [#mark-span inspector type: "block_annotation" span: block kind: "affector" message: "This block affects the inspected value."] + [#jump-to span: block] +~~~ + +When the `"annotate-affectors"` state is active, annotate blocks which could draw to the document root. + +~~~ +search @inspector + inspector = [#inspector state: "annotate-affectors" root] + +search @editor + [#root-drawer span start stop] + +bind @editor + [#mark-range inspector type: "annotation" span start stop kind: "affector" message: "This block could draw to the document root."] + [#jump-to position: start] +~~~ + +### Focus Failures + +When the `"focus-failures"` state is active, elide everything but the blocks which could impact the target but did not. + +~~~ +search @inspector + inspector = [#inspector state: "focus-failures" element] + +search @editor + [#affector record: element block action] + [#failure block] + +bind @editor + [#elide-between-sections inspector | span: block] + [#jump-to position: 0] + [#mark-span inspector type: "highlight" | span: action] +~~~ + +### Annotate Failures + +When the `"annotate-failures"` state is active, annotate the blocks which could impact the target. + +~~~ +search @inspector + inspector = [#inspector state: "annotate-failures" element] + +search @editor + [#affector record: element block action] + [#failure block] + +bind @editor + [#mark-span inspector type: "block_annotation" span: block kind: "failure" message: "This block could affect the inspected value but failed."] + [#jump-to span: block] +~~~ + +### Highlight Failures + +When the `"highlight-failures"` state is active, show the failing reasons for blocks that impact the target. + +~~~ +search @inspector + inspector = [#inspector state: "highlight-failures" element] + +search @editor + [#affector record: element block] + [#failure block start stop] + +bind @editor + [#mark-range type: "cause-of-failure" start stop] +~~~ + +### Highlight Block Failure + +When the `"highlight-block-failure"` state is active, show the failing reason for the targeted block. + +~~~ +search @inspector + inspector = [#inspector state: "highlight-block-failure" block] + +search @editor + [#failure block start stop] + +bind @editor + [#mark-range type: "cause-of-failure" start stop] +~~~ + +### Annotate Performance + +~~~ +search @inspector + inspector = [#inspector state: "annotate-performance"] + +search @editor + [#performance block color] + color != "green" + +bind @editor + [#mark-span inspector type: "block_annotation" span: block kind: "performance-{{color}}" message: "This block has performance issues."] +~~~ + +~~~ +search @inspector + inspector = [#inspector state: "annotate-performance"] + +search @editor + perf = [#performance block color] + total-percent = to-fixed[value: perf.percent, places: 2] + average-time = to-fixed[value: perf.average, places: 2] + max-time = to-fixed[value: perf.max, places: 2] + +bind @editor + [#mark-span inspector type: "document_widget" span: block kind: "performance-{{color}}" message: "{{total-percent}}% of total | average {{average-time}}ms | max {{max-time}}ms"] + +~~~ + +### Focus Performance + +~~~ +search @inspector + inspector = [#inspector state: "focus-performance"] + +search @editor + [#performance block color != "green"] + +bind @editor + [#elide-between-sections inspector | span: block] + [#jump-to position: 0] +~~~ + +## Drawing + +While we have an inspector, notify the editor about its existence. + +~~~ +search @inspector + inspector = [#inspector x y] + +bind @browser + inspector <- [#editor #inspector x y: y + 15] +~~~ + +When an `#inspector` is open targeted to an element, show the element popout. + +~~~ +search @inspector + inspector = [#inspector state: "popout" element] + +bind (@view, @browser) + attributes = [#attribute-table inspector entity: element] + +bind @browser + inspector <- + [#div element class: "inspector-pane" children: + [#div #attributes-panel inspector children: attributes] + [#div #options-panel inspector class: "buttons" children: + [#button inspector text: "Find events" activate: "focus-blocks", deactivate: "popout"] + [#button inspector text: "Why this?" activate: "show-values-table" deactivate: "popout"] + [#button inspector text: "Too few? Too many?" activate: "show-cardinalities-inline" deactivate: "popout"] + [#button inspector text: "Not drawing?" activate: ("annotate-affectors", "highlight-failures") deactivate: ("annotate-blocks", "popout")]]] +~~~ + +When an `#inspector` is open targeted to the document root, show the root popout. + +~~~ +search @inspector + inspector = [#inspector state: "popout" root] + +bind @browser + inspector <- [#div class: "inspector-pane" children: + [#div #options-panel inspector class: "buttons" children: + [#button inspector text: "Not drawing?" activate: ("annotate-affectors", "highlight-failures") deactivate: "popout"]]] +~~~ + +When an `#inspector` is open targeted to a block, show the block popout. +@TODO: Just make this the popout for blocks. + +~~~ +search @inspector + inspector = [#inspector state: "block-popout" block] + +bind @browser + inspector <- [#div in-editor: true class: "inspector-pane" children: + [#div #options-panel inspector class: "buttons" children: + [#button inspector text: "Did this fail?" activate: "highlight-block-failure" deactivate: "block-popout"] + [#button inspector text: "Why is this slow?" activate: "annotate-performance" deactivate: "block-popout"]]] + +~~~ + +## Debug + +### Inspector Debugger + +~~~ +search @view + wrapper = [#inspector-debugger inspector] + lookup[record: wrapper, attribute: "tag", value: "inspector-debugger", node] + +search (@session, @browser, @inspector) + lookup[record: inspector attribute: key value] + +bind @view @browser + wrapper.node := node + wrapper.tag += "kv-table" + wrapper.kvs := [key value] +~~~ + + + +## Example Scenarios + +### Missing Attribute + +*"I shouldn't be able to see this button when I'm not logged in."* + +1. Click the investigate button. +2. Click the logout button. + - Should this not show up right now? + - Are there too many or few of these? + - Does this have an incorrect value? + - List of attributes +3. This should not show up right now. + - Filter code to blocks which create the logout button. + - Show a timeline of events creating the button. + - The `[#button #log-out]` is drawn because `app.user` is tagged `#logged-in`. + - The `app.user` is tagged `#logged-in` because the `[#button #login]` was clicked. + - List blocks which could cause it to stop drawing. + - The `app.user` `#logged-in` tag was not removed when `[#button #logout]` was clicked because `app.user` did not have an `email` attribute. + +### Bad Join + +*"The student attendance list is missing."* + +1. Click the investigate button. +2. Click where the attendance list belongs. + - Qs +3. This should contain something. + - Filter code to blocks which can create children for this element. + - Highlight the bailing reason for each block. + - Summary + - [this block](#) can insert here, but `[#app page: not(grades)]` + - [this block](#) can insert here, but `[#app page: not(syllabus)]` + - [this block](#) can insert here, but `teacher.students` is empty +4. Click `teacher.students` + - Filter code to the blocks affecting `teacher.students` + - [this block](#) creates `teacher.students`, but `not(student.name = school.name)` + + +### Wrong Cardinality + +*"Too many emails were sent to each client."* + +1. Click the investigate button. +2. Click an email to a client. + - Should this not show up right now? + - Are there too many or few of these? + - Does this have an incorrect value? + - List of attributes +3. There are too many of these. + - Filter code to the block which created the emails. + - Gray out the patterns that do not contribute to the cardinality of the emails. + - Embed a cardinality badge for each relevant pattern. + - Summary + - an email was created for each `[#client email]` (721) X `[#promo message]` (1) X `[#admin email]` (5). + - Click a pattern to see its values. +4. Click `[#admin email]`. + - There are 5 `[#admin]`s, each with one `email`. These are: ... + +### Wrong Value + +*"The clock hand in the wrong position."* + +1. Click the investigate button. +2. Click the clock hand. + - Should this not show up right now? + - Are there too many or few of these? + - Does this have an incorrect value? + - List of attributes +3. Click the `x2` attribute. + - Filter code to the blocks affecting the `x2` attribute of the entity. + - Gray out the patterns that do not contribute to the `x2` attribute. + - Embed value badges for each relevant variable. + - Summary + - `[#clock-hand degrees` (70) `length` (30) `]`. + - `x2` (78.19) `= 50 + (length` (30) `* sin[degrees` (70) `]` (0.94) `)`. + - `hand <- [#line x1: 50, y1: 50, x2` (78.19) `y2]`. +4. Click the `degrees` attribute. + - Filter code to the blocks affecting the `degrees` attribute of the `[#clock-hand]` + - Gray out the patterns that do not contribute to the `degrees attribute`. + - Embed value badges for each relevant variable. + - Summary + - `[#clock-hand #hour-hand degrees` (900)`: 60 * hours` (15) `, length: 30, stroke: "#023963"]` + +### Performance + +*"This program slow."* + +1. Click the investigate button. +2. ??? +3. Color-code blocks by total time spent. +4. Click a block. + - Display total percentage of time spent. + - Display total number of rows run. + - Display average/max/min time per row. + +### Exploration + +*"What has this block done?"* +1. Click the investigate button. +2. Click the block + - What does this depend on? + - What does this create? + - What depends on this? +3. What does this create? + - List patterns and cardinalities created by this node + - Highlight visible side effects (e.g. ui) + +*"What created this?"* +1. Click the investigate button. +2. Click the element or pattern + - What created this? +3. What created this? + - Filter code to blocks affecting this. + - Highlight the specific actions impacting this. + + +## Inspector views + +- Element +- Variable +- Action +- Scan +- Block + +- Active element + - Something's wrong + - Are there too many/few of these? + - Is a value wrong? + - Is something missing? + - Should this not be here? (?) + - Exploration + - source + - dependents + +- Active variable + - Something's wrong + - Are there too many/few of these? diff --git a/examples/quickstart.eve b/examples/quickstart.eve new file mode 100644 index 000000000..2c60316a9 --- /dev/null +++ b/examples/quickstart.eve @@ -0,0 +1,275 @@ +# Eve Quick Start Tutorial + +~~~eve +bind @browser + [tag: "div", text: "Hello, world"] +~~~ + +Hello world! At its core, Eve is a pattern matching language. You match patterns of data by searching a database, then update or create new data according to what you've found. In this example, we created a [record][records] that has two attributes: a tag attribute with the value `"div"`, and a text attribute with the value `"Hello, world"`. We [bound][bind] this record to the browser, which is how we displayed our venerable message. + +Eve code is fenced off in blocks, which can be written in any order and embedded in Markdown documents. This is how Eve programs are written: everything in a code fence is a [block][blocks] of Eve code, while everything outside is prose describing the program. In fact, this quick start tutorial is an example of an executable Eve program! In the subsequent blocks, you won't see any code fences, but they still exist in the [document's source][quickstart-source]. You may have noticed the checkbox in the top right corner of the code block. In Eve, any block can be disabled in one click. As you work through the quickstart, enable each block you reach to follow along. + +So far we've created a record that displays "Hello, world!" but as I said, Eve is a pattern matching language. Let's explore that by searching for something: + +~~~eve disabled +search + [name] + +bind @browser + [tag: "div", text: "Ciao, world"] +~~~ + +Our message never appeared! Before, we bound without searching, so the message displayed by default. Now we're binding in the presence of a [search][search] action, so the bound record only exists if all the searched records are matched. Here, we're searching for all records with a `name` attribute, but we haven't added any records like that to Eve; so none are matched. With no matching records, the `bind` cannot execute, and the message disappears from the screen. + +This is the flow of an Eve block: you search for records in a database, and if all the records you searched for are matched, you can modify the matched records or create new ones. If any part of your search is not matched, then no records will be created or updated. + +To get our new message to show up, all we need is a record with a name attribute. We can create one permanently with the [commit][commit] action: + +~~~eve disabled +commit + [name: "Celia"] +~~~ + +Hello, world… again! Commit permanently updates or creates a record that will persist even if its matched records (the records matched in a search action) change. Since we aren't searching for anything in this block, the commit executes by default and adds a record with a name attribute of `"Celia"`. The addition of this new record satisfies the search in the previous block, so "Ciao, world!" appears on the screen along with the first message. + +But what else can you do with matched records? For starters, we can use them to create more records: + +~~~eve disabled +search + [name] + +bind @browser + [#div, text: "Hello, {{name}}"] +~~~ + +Since we matched on a record with a name attribute, we now have a reference to that name, and we can inject it into a string using [{{ ... }}][string-interpolation] embedding. Since tags are used so commonly in Eve, we can swap out `tag: "div"` for its syntax-sugared form `#div`. [Tags][tags] are often used to talk about groups of related records. For example, we could search for all records with a `#student` tag, with name, grade, and school attributes. + +~~~eve disabled +search + [#student name grade school] + +bind @browser + [#div text: "{{name}} is a {{grade}}th grade student at {{school}}."] +~~~ + +Since we're matching on more attributes, this block is no longer satisfied by the record we added earlier. We're missing a `#student` tag, as well as grade and school attributes. Even though these are currently missing, we can still write the code that would display them as if they existed. + +Let's display this new message by adding the missing attributes to Celia. We could add them to the block where we comitted Celia originally, but we can also do it programatically: + +~~~eve disabled +search + celia = [name: "Celia"] + +bind + celia <- [#student grade: 10, school: "East", age: 16] +~~~ + +You can define variables within blocks, which act as handles on records that allow you to change them. In this case, we're using the [merge operator][merge] `<-` to combine two records. With the addition of this block, the sentence "Celia is a 10th grade student at East." appears in the browser. + +Celia is cool and all, but let's add a few more students to our database: + +~~~eve disabled +commit + [#student name: "Diedra", grade: 12, school: "West"] + [#student name: "Michelle", grade: 11, school: "West"] + [#student name: "Jermaine", grade: 9] +~~~ + +Three sentences are now printed, one for each student that matches the search. Eve works on [sets][sets], so when we search for `[#student name grade school]`, we find _all_ records that match the given pattern. This includes Celia, Diedra and Michelle (but not Jermaine, as he has no school in his record). Therefore, when we bind the record `[#div text: "{{name}} is a ... "]`, we are actually binding three records, one for each matching `#student`. + +If you re-compile the program a couple times, you'll see the order of sentences may change. This is because **there is no ordering in Eve - blocks are not ordered, statements are not ordered, and results are not ordered**. If you want to order elements, you must impose an ordering yourself. We can ask the browser to draw elements in an order with the "sort" attribute: + +~~~eve disabled +search + [#student name grade school] + +bind @browser + [#div sort: name, text: "{{name}} is a {{grade}}th grade student at {{school}}."] +~~~ + +This time when you recompile your program, the order will stay fixed, sorted alphabetically by name. + +Let's make things a little more interesting by adding some records about the schools the students attend: + +~~~eve disabled +commit + [#school name: "West", address: "1234 Main Street"] + [#school name: "East", address: "5678 Broad Street"] +~~~ + +What if we want to display the address of the school each student attends? Although `#student`s and `#school`s are in different records, **we can relate two records by associating attributes from one record with attributes from the other.** This is an operation known as [joining][joins]. In this case, we want to relate the `name` attribute on `#schools` with the `school` attribute on `#students`. This compares the values of the attributes between records, and matches up those with the same value. For instance, since Celia's school is "East", she can join with the `#school` named "East". + +Our first attempt may come out looking a little something like this: + +~~~eve disabled +search + school = [#school name address] + student = [#student name school: name] + +bind @browser + [#div text: "{{student.name}} attends {{school.name}} at {{address}}"] +~~~ + +But that didn't work. How come? In Eve, **things with the same name are [equivalent][equivalence]**. In this block, we've used "name" three times, which says that the school's name, the student's name, and the student's school are all the same. Of course, there is no combination of students and schools that match this search, so nothing is displayed. + +Instead, we can use the dot operator to specifically ask for the name attribute in the `#school` records, and rename our variables to get a correct block: + +~~~eve disabled +search + schools = [#school address] + students = [#student school: schools.name] + +bind @browser + [#div text: "{{students.name}} attends {{schools.name}} at {{address}}"] +~~~ + +This creates an implicit join over the school name without mixing up the names of the students and the names of the schools, giving us our desired output. You can actually bind attributes to any name you want to avoid collisions in a block: + +~~~eve disabled +search + [#school name: school-name address] + [#student name: student-name school: school-name] + +bind @browser + [#div text: "{{student-name}} attends {{school-name}} at {{address}}"] +~~~ + +## Advanced Eve + +Recall when we added our students, Celia was the only one we added an `age` to. Therefore, the following block only displays Celia's age, even though we ask for all the `#student`s: + +~~~eve disabled +search + [#student name age] + +bind @browser + [#div text: "{{name}} is {{age}} years old"] +~~~ + +Let's pretend that all students enter first grade at six years old. Therefore, if we know a student's grade, we can calculate their age and add it to the student's record: + +~~~eve disabled +search + student = [#student] + calculated-age = if student.age then student.age + else if student.grade then student.grade + 5 + +bind + student.age := calculated-age +~~~ + +This block selects all students, and uses and [if-then][if-then] expression to set the student's calculated age. If the student already has an age, we set it to that. Otherwise, if the student has no age, we can calculate it with some arithmetic. The [set operator][set] `:=` sets an attribute to a specified value regardless of what it was before the block executed. That value can be anything, from a number to a string to another record. + +### Aggregates + +So far everything we've done has used one record at a time, but what happens when we want to work over a group of records, such as counting how many students there are? To solve such a problem, we'll need to use an [aggregate][aggregates]. Aggregates take a set of values and turn them into a single value, akin to "fold" or "reduce" functions in other languages. In this case, we'll use the aggregate [count][count] to figure out how many `#students` are in the school district: + +~~~eve disabled +search + students = [#student] + total-students = count[given: students] + +bind @browser + [#div text: "{{total-students}} are in the school district"] +~~~ + +A quick note on the syntax for `count` - it feels a lot like a function in other languages, since it has a return value and can be used inline in expressions. Under the hood, [functions][functions] and aggregates are actually records; `total = count[given: students]` is shorthand for `[#count #function given: students, value: total]`. This distinction won't materially change the way you use `count`, but it goes to show that everything in Eve reduces to working with records. + +While `given` is a required argument in `count`, aggregates (and functions in general) can also have optional arguments. Let's say we want to know how many students attend each school. We can use the optional argument `per` to count students grouped by the school they attend: + +~~~eve disabled +search + students = [#student school] + students-per-school = count[given: students, per: school] + +bind @browser + [#div text: "{{students-per-school}} attend {{school}}"] +~~~ + +All function-like records in Eve specify their arguments as attributes. This means you specify the argument and its value, unlike in other languages, where the order of the values determines the attribute to which they belong. As with everything else in Eve, order doesn't matter. + +## Extra Credit + +At this point, you know everything necessary about Eve to complete this extra credit portion (the only additional knowledge you need is domain knowledge of HTML and forms). Let's review some of the key concepts: + +- Eve programs are composed of blocks of code that search for and update records. +- Records are sets of `attribute: value` pairs attached to a unique ID. +- Eve works with sets, which have no ordering and contain unique elements. +- Things with the same name are equivalent. + +Your extra credit task is to build a web-based form that allows you to add students to the database. Take a moment to think about how this might be done in Eve, given everything we've learned so far. + +First, let's make the form. We've already displayed a `#div`, and in the same way we can draw `#input`s and a `#button`: + +~~~eve disabled +bind @browser + [#div children: + [#div sort: 1, text: "Name:"] + [#input #name-input sort: 2] + [#div sort: 3, text: "Grade:"] + [#input #grade-input sort: 4] + [#div sort: 5, text: "School:"] + [#input #school-input sort: 6] + [#button #submit sort: 7 text: "submit"]] +~~~ + +We've added some tags to the inputs and the button to distinguish them, so we can easily search for them from other blocks. Now that we have a form, we need to define what happens when the submit button is clicked. + +Remember, everything in Eve is a record, so the `#click` event is no different. When a user clicks the mouse in the browser, Eve records that click in the database. + +This record exists only for an instant, but we can react to it by searching for `[#click element: [#submit]]`. This record represents a `#click` on our `#submit` button. Then, all we need to do is capture the values of the input boxes and save them as a `#student` record: + +~~~eve disabled +search @browser @event + [#click element: [#submit]] + name = [#name-input] + grade = [#grade-input] + school = [#school-input] + grade-number = convert[value: grade.value to: "number"] + +commit + // save the new student + [#student name: name.value, grade: grade-number, school: school.value] + +commit @browser + // reset the form + name.value := "" + grade.value := "" + school.value := "" +~~~ + +## Learning more + +If you want to learn more about Eve, we have some resources to help with that: + +- Example applications - See some working programs and explore how they work. +- Tutorials - Step by step instructions on building Eve applications. +- [The Eve Handbook](https://witheve.github.io/docs) - Everything you need to know about Eve. +- [Eve syntax reference](https://witheve.github.io/assets/docs/SyntaxReference.pdf) - Eve's syntax in one page. +- Guides - In-depth documents on topics relating to Eve. + +We also invite you to join the Eve community! There are several ways to get involved: + +- Join our [mailing list](https://groups.google.com/forum/#!forum/eve-talk) and get involved with the latest discussions on Eve. +- Impact the future of Eve by getting involved with our [Request for Comments](https://github.com/witheve/rfcs) process. +- Read our [development diary](http://incidentalcomplexity.com/) for the latest news and articles on Eve. +- Follow us on [twitter](https://twitter.com/with_eve). + + +[records]: https://witheve.github.io/docs/handbook/records/ +[blocks]: https://witheve.github.io/docs/handbook/blocks/ +[search]: https://witheve.github.io/docs/handbook/search/ +[bind]: https://witheve.github.io/docs/handbook/bind/ +[commit]: https://witheve.github.io/docs/handbook/commit/ +[string-interpolation]: https://witheve.github.io/docs/handbook/string-interpolation/ +[joins]: https://witheve.github.io/docs/handbook/joins/ +[quickstart-source]: https://github.com/witheve/Eve/blob/ts-merge/examples/quickstart.eve +[tags]: https://witheve.github.io/docs/handbook/tags/ +[merge]: https://witheve.github.io/docs/handbook/merge/ +[sets]: https://witheve.github.io/docs/handbook/sets/ +[equivalence]: https://witheve.github.io/docs/handbook/equivalence/ +[if-then]: https://witheve.github.io/docs/handbook/if-then/ +[set]: https://witheve.github.io/docs/handbook/set/ +[aggregates]: https://witheve.github.io/docs/handbook/aggregates/ +[functions]: https://witheve.github.io/docs/handbook/functions/ +[count]: https://witheve.github.io/docs/handbook/statistics/count/ diff --git a/examples/server.eve b/examples/server.eve new file mode 100644 index 000000000..a52b331c3 --- /dev/null +++ b/examples/server.eve @@ -0,0 +1,31 @@ +# Server Example + +When `@server` receives a `#request`, it responds with a "yo!" and displays the requsted URL and who requested it. + +~~~ +search @server + r = [#request url headers: [user-agent]] + +commit @server + r.response := [#response status: 200, body: "yo!"] + +bind @browser + [#div text: "Request for: {{url}} - {{user-agent}}"] +~~~ + +When the server gets a `#request` from slack, it's assumed to be a message with some text from a user, which is echoed back to the server from user `meep`. + +~~~ +search @server + r = [#request url: "/slack-message" body: [text user_name]] + +bind @browser + [#div text: "Body for slack message is {{user_name}}: \"{{text}}\""] + +commit @http + [#request from: r, + headers: [meep: "moop", beep: "boop"] + method: "POST" + url: "https://hooks.slack.com/services/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" + json: [text: "{{user_name}} said: `{{text}}`", icon_emoji: ":wizard:", username: "meep"]] +~~~ diff --git a/examples/tic-tac-toe.eve b/examples/tic-tac-toe.eve new file mode 100644 index 000000000..299b9b9b0 --- /dev/null +++ b/examples/tic-tac-toe.eve @@ -0,0 +1,214 @@ +# Tic-Tac-Toe + +## Game logic + +Tic-Tac-Toe is a classic game played by two players, "X" and "O", who take turns marking their letter on a 3x3 grid. The first player to mark 3 adjacent cells in a line wins. The game can potentially result in a draw, where all grid cells are marked, but neither player has 3 adjacent cells. To build this game in Eve, we need several parts: + +- A game board with cells +- A way to mark a cell as "X" or "O" +- A way to recognize that a player has won the game. + +### Game settings + +To begin, we initialize the board. We commit an object named @board to hold our global state and create a set of #cells. These #cells will keep track of the moves players have made. Common connect-N games (a generalized tic-tac-toe for any `NxN` grid) are scored along 4 axes (horizontal, vertical, the diagonal, and the anti-diagonal). We group cells together along each axis up front to make scoring easier later. + +The game board is square, with a given size. It contains size ^ 2 cells, +each with a row and column index. + +~~~ +search + // board constants + size = 3 + starting-player = "X" + + // generate the cells + i = range[from: 1, to: size] + j = range[from: 1, to: size] + +commit + board = [#board size player: starting-player] + [#cell board row: i column: j] +~~~ + +A subtlety here is the last line, [#cell board row: i column: j]. Thanks to our relational semantics, this line actually generates all 9 cells. Since the sets of values computed in i and j have no relation to each other, when we use them together we get the cartesian product of their values. This means that if `i = {0, 1, 2}` and `j = {0, 1, 2}`, then `i x j = {(1, 1), (1, 2), ... (3, 2), (3, 3)}`. These are exactly the indices we need for our grid! + +Now we tag some special cell groupings: diagonal and anti-diagonal cells. The diagonal cells are `(1, 1)`, `(2, 2)`, and `(3, 3)`. From this we can see that diagonal cells have a row index equal to its column index + +~~~ +search + cells = [#cell row column] + row = column + +bind + cells += #diagonal +~~~ + +Similarly, the anti-diagonal cells are `(1, 3)`, `(2, 2)`, and `(3, 1)`. + +Anti-diagonal cells satisfy the equation `row + col = N + 1`, +where `N` is the size of the board. + +~~~ + search + cells = [#cell row column] + [#board size: N] + row + column = N + 1 + + bind + cells += #anti-diagonal +~~~ + +### Winning condition + +A game is won when a player marks N cells in a row, column, or diagonal. +The game can end in a tie, where no player has N in a row. + +~~~ +search + board = [#board size: N, not(winner)] + + (winner, cell) = + // Check for a winning row + if cell = [#cell row player] + N = count[given: cell, per: (row, player)] + then (player, cell) + // Check for a winning column + else if cell = [#cell column player] + N = count[given: cell, per: (column, player)] + then (player, cell) + // Check for a diagonal win + else if cell = [#diagonal row column player] + N = count[given: cell, per: player] + then (player, cell) + // Check for an anti-diagonal win + else if cell = [#anti-diagonal row column player] + N = count[given: cell, per: player] + then (player, cell) + // If all cells are filled but there are no winners + else if cell = [#cell player] + N * N = count[given: cell] + then ("nobody", cell) + +commit + board.winner := winner + cell += #winner +~~~ + +We use the count aggregate in the above block. Count returns the number of discrete values (the cardinality) of the variables in given. The optional per attribute allows you to specify groupings, which yield one result for each set of values in the group. + +For example, in count[given: cell, per: player] we group by player, which returns two values: the count of cells marked by player X and those marked by O. This can be read "count the cells per player". In the scoring block, we group by column and player. This will return the count of cells marked by a player in a particular column. Like wise with the row case. By equating this with N, we ensure the winning player is only returned when she has marked N cells in the given direction. + +This is how Eve works without looping. Rather than writing a nested for loop and iterating over the cells, we can use Eve's semantics to our advantage. + +We first search every row, then every column. Finally we check the diagonal and anti-diagonal. To do this, we leverage the `#diagonal` and `#anti-diagonal` tags we created earlier; instead of searching for `#cell`, we can search for these new tags to select only a subset of cells. + +## React to Events + +Next, we handle user input. Any time a cell is directly clicked, we: + +- Ensure the cell hasn't already been played +- Check for a winner +- Switch to the next player + +Then update the cell to reflect its new owner, and switch board's player to the next player. + +### Marking a cell + +Click on a cell to make your move + +~~~ +search @event @session @browser + [#click #direct-target element: [#div cell]] + // Ensures the cell hasn't been played + not(cell.player) + // Ensures the game has not been won + board = [#board player: current, not(winner)] + // Switches to the next player + next_player = if current = "X" then "O" + else "X" + +commit + board.player := next_player + cell.player := current +~~~ + +### Reset the game + +Since games of tic-tac-toe are often very short and extremely competitive, it's imperative that it be quick and easy to begin a new match. When the game is over (the board has a winner attribute), a click anywhere on the drawing area will reset the game for another round of play. + +A reset consists of: + +- Clearing the board of a winner +- Clearing all of the cells +- Removing the #winner tag from the winning cell set + +~~~ +search @event @browser @session + [#click element: [#div board]] + board = [#board winner] + cell = [#cell player] + +commit + board.winner -= winner + cell.player -= player + cell -= #winner +~~~ + +## Drawing the Game + +We've implemented the game logic, but now we need to actually draw the board so players have something to see and interact with. Our general strategy will be that the game board is a #div with one child `#div` for each cell. Each cell will be drawn with an "X", "O", or empty string as text. We also add a `#status` div, which we'll write game state into later. Our cells have the CSS inlined, but you could just as easily link to an external file. + +### Draw the board + +~~~ +search + board = [#board] + cell = [#cell board row column] + contents = if cell.player then cell.player + else "" + +bind @browser + [#div board #container style: [font-family: "sans-serif"], children: + [#div #status board class: "status", style: [text-align: "center" width: 150 height: 50, padding-bottom: 10]] + [#div class: "board", style: [color: "black"] children: + [#div class: "row", sort: row, children: + [#div #cell cell class: "cell", text: contents, sort: column, + style: [display: "inline-block", width: 50, height: 50, border: "1px solid black", background: "white", font-size: "2em", line-height: "50px", text-align: "center", vertical-align: "top"]]]]] +~~~ + +Winning cells are drawn in a different color + +~~~ +search @session @browser + winning-cells = [#cell #winner] + cell-elements = [#div cell: winning-cells style] + +bind @browser + style.color := "blue" +~~~ + +### Draw status message + +Finally, we fill the previously mentioned #status div with our current game state. If no winner has been declared, we remind the competitors of whose turn it is, and once a winner is found we announce her newly-acquired bragging rights. + +Display the current player if the game isn't won + +~~~ +search @session @browser + status = [#status board] + not(board.winner) + +bind @browser + status.text += "It's {{board.player}}'s turn!" +~~~ + +When the game is won, display the winner + +~~~ +search @session @browser + status = [#status board] + winner = board.winner + +bind @browser + status.text += "{{winner}} wins! Click anywhere to restart!" +~~~ diff --git a/examples/todo-list.eve b/examples/todo-list.eve new file mode 100644 index 000000000..aeec94f73 --- /dev/null +++ b/examples/todo-list.eve @@ -0,0 +1,55 @@ +# Todo List Application +## Records +### Todos +Filling the todo list with some objects: +``` +commit + [#todo status: "unresolved", text: "buy potatos"] + [#todo status: "resolved", text: "buy wine"] + [#todo status: "unresolved", text: "buy water"] +``` + +## Pages +### Unresolved List +Follows a list of unresolved todos: +```eve +search + [#todo text status: "unresolved"] + +bind @browser + [#div text: "Unresolved Todos"] + [#li text: "{{text}}"] +``` + +### Resolved List +Follows a list of resolved todos: +``` +search + [#todo status text status: "resolved"] + +bind @browser + [#div text: "Resolved Todos"] + [#li text: "{{text}} - {{status}}"] +``` + +### Todo List +Follows the todo list: +``` +search + todo = [#todo text status] + +bind @browser + [#div text: "Todo List"] + [#li text: "{{todo.text}} - {{todo.status}}" element: [#button text: "wat"]] +``` + +## Events +### Todo Event Click +Mark a todo element as resolved: +``` +search @event @browser @session + [#click #direct-target element: [#button text]] + +commit + +``` diff --git a/examples/todomvc.eve b/examples/todomvc.eve new file mode 100644 index 000000000..8e35c98c8 --- /dev/null +++ b/examples/todomvc.eve @@ -0,0 +1,193 @@ +# TodoMVC + +[TodoMVC][1] is a specification for a todo list web application. Its original purpose was to help developers evaluate the plethora of Javascript frameworks implementing the Model-View-Controller (MVC) design pattern. Today, it has evolved into a benchmark for programming languages and frameworks targeting the web, so TodoMVC apps don’t necessarily reflect the MVC design pattern. +TodoMVC is a great example to demonstrate Eve's capabilities, as our semantics naturally enable a concise implementation; without optimizing for line count, the program you're reading only contains 63 lines of Eve code. + +## Todos + +Each todo is tagged `#todo` and has a `body`, which is the text of the todo entered by the user. Additionally, each todo has two flags. The first flag is the `completed` flag affects how the todo is displayed, and allows the user to filter todos based on completed status; we can look at "completed" todos, "active" todos, or "all" todos. The second flag is the `editing` flag, used to toggle an editing mode on the todo. This is used later to allow the user to update the body text of the todo. + +These todos exist in the context of an `#app`, which we use to hold global state information. We use it to place a filter on the todos. The filter can be one of "completed", "active", or "all". + +### The Application View + +We draw Todo MVC here. All styling is handled in a separate CSS file. The app consists of three parts: + +1. __Header__ - Contains the `#toggle-all` button as well as `#new-todo`, which is an input box for entering new todos. +2. __Body__ - Contains `#todo-list`, the list of todos. The work here is handled in the second block. +3. __Footer__ - Contains the count of todos, as well as control buttons for filtering, and clearing completed todos. + +In this block, we do a little work to determine todo-count, all-checked, and none-checked. Other than that, this block simply lays out the major control elements of TodoMVC. A key aspect of this block is the `bind` keyword. This denotes the beginning of the action phase of the block, and tells Eve that to update records as data changes. This is the key component that enables Eve to react to user interaction and update the display. + +~~~ +search + [#app filter] + + all-checked = if not([#todo completed: false]) then true + else false + + none-checked = if [#todo completed: true] then false + else true + + todo-count = if c = count[given: [#todo completed: false]] then c + else 0 + +bind @browser + // Links to an external stylesheet + [#link rel: "stylesheet" href: "examples/css/todomvc.css"] + [#div class: "todoapp" children: + + [#header children: + [#h1 text: "todos"] + [#input #new-todo, class: "new-todo", autofocus: true, + placeholder: "What needs to be done?"] + [#input #toggle-all class: "toggle-all", type: "checkbox", + checked: all-checked]] + + [#div class: "main" children: + [#ul #todo-list, class: "todo-list"]] + + [#footer children: + [#span #todo-count, class: "todo-count", children: + [#strong text: todo-count] + [#span text: " items left"]] + [#ul #filters, class: "filters", children: + [#li children: [#a href: "#/examples/todomvc.eve/#/all" text: "all" + class: [selected: is(filter = "all")]]] + [#li children: [#a href: "#/examples/todomvc.eve/#/active" text: "active" + class: [selected: is(filter = "active")]]] + [#li children: [#a href: "#/examples/todomvc.eve/#/completed" text: "completed" + class: [selected: is(filter = "completed")]]]] + [#span #clear-completed text: "Clear completed" + class: [clear-completed: true, + hidden: none-checked]]]] +~~~ + +### Drawing the Todo List + +Now we look at how the todos are actually displayed in the application. We attach it to `#todo-list` using its `children` attribute. Each todo display element consists of: + +- a **list item**, with a checkbox for toggling the completed status of the todo +- a **label** displaying the text of the todo +- an **input textbox** for editing the text of the todo +- a **button** for deleting the todo + +~~~ +search @session @browser + [#app filter] + parent = [#todo-list] + + (todo, body, completed, editing) = + if filter = "completed" + then ([#todo, body, completed: true, editing], body, true, editing) + + else if filter = "active" + then ([#todo, body, completed: false, editing], body, false, editing) + + else if filter = "all" + then ([#todo, body, completed, editing], body, completed, editing) + +bind @browser + parent.children += + [#li, class: [todo: true, completed, editing], todo, children: + [#input #todo-checkbox todo type: "checkbox" checked: completed + class: [toggle: true, hidden: editing]] + [#label #todo-item, class: [hidden: editing], todo, text: body] + [#input #todo-editor, todo, value: body, autofocus: true + class: [edit: true, + hidden: toggle[value: editing]]] + [#button #remove-todo, class: [destroy: true, hidden: editing], todo]] +~~~ + +Thanks to Eve's set semantics, we don't need any loops here; for every unique `#todo` in the database, Eve will do the work of adding another `#li` as a child of `#todo-list`. + +## Responding to User Events + +### Creating a New Todo + +A user can interact with TodoMVC in several ways. First and foremost, the user can create new todos. When the `@new-todo` input box is focused and the user presses enter, the value of the input is captured and a new todo is created. + +~~~ +search @event @session @browser + element = [#new-todo value] + kd = [#keydown element, key: "enter"] + +commit + [#todo body: value, editing: false, completed: false, kd] + +commit @browser + element.value := "" +~~~ + +Of note here is the record `[#todo body: value, editing: false, completed: false, kd]`. The inclusion of the `kd` attribute might seem strange, but its purpose is to guarantee the uniqueness of the todo. Let’s say we want to add two todos with the same body. If `kd` were not an attribute, then the two todos would be exactly the same and Eve’s set semantics would collapse them into a single todo. Therefore, we need some way to distinguish todos with identical bodies. Adding `kd` allows for this. + +### Editing Todos + +Here we handle all the ways we edit a todo. Editing includes changing the body as well as toggling the status of between complete and active. + +- click `#todo-checkbox` - toggles the completed status of the checkbox. +- click `#toggle-all` - marks all todos as complete or incomplete, depending on the initial value. If all todos are marked incomplete, clicking `#toggle-all` will mark them complete. If only some are marked complete, then clicking `#toggle-all` will mark the rest complete. If all todos are marked as complete, then clicking `#toggle-all` will mark them all as incomplete. +- blur `#todo-editor` - blurring the `#todo-editor` will cancel the edit +- escape `#todo-editor` - this has the same effect as blurring +- enter `#todo-editor` - commits the new text in `#todo-editor`, replacing the original body + +~~~ +search @event @session @browser + (todo, body, editing, completed) = + if [#click element: [#todo-checkbox todo]] + then (todo, todo.body, todo.editing, toggle[value: todo.completed]) + + else if [#click element: [#toggle-all checked]] + then ([#todo body], body, todo.editing, toggle[value: checked]) + + else if [#double-click element: [#todo-item todo]] + then (todo, todo.body, true, todo.completed) + + else if [#blur element: [#todo-editor todo value]] + then (todo, value, false, todo.completed) + + else if [#keydown element: [#todo-editor todo] key: "escape"] + then (todo, todo.body, false, todo.completed) + + else if [#keydown element: [#todo-editor todo value] key: "enter"] + then (todo, value, false, todo.completed) + +commit + todo <- [body, completed, editing] +~~~ + +### Deleting Todos + +We remove a todo from the list by setting the todo's record to special `none` value. Doing so completely erases that todo from the database. + +~~~ +search @event @session @browser + todo = if [#click element: [#remove-todo todo]] + then todo + else if [#click element: [#clear-completed]] + then [#todo completed: true] + +commit + todo := none +~~~ + +### Filtering Todos (Routing) + +The TodoMVC specification requires filtering via the URL. This is actually how the filter buttons work; if you look at their href attributes, they modify the URL with certain tags: + +- all - displays all todos +- active - displays active todos only +- completed - displays completed todos only + +We can extract this value using `#url`, which has a hash-segment attribute that automatically parses the URL for us, returning the `value` (expected to be any one of the above). Any other value will fail to show any todos, but the application will not break. + +~~~ +search @browser + value = if [#url hash-segment: [index: 1, value]] + then value + else "all" +bind + [#app filter: value] +~~~ + +[1]: http://todomvc.com/ diff --git a/examples/view.eve b/examples/view.eve new file mode 100644 index 000000000..6f527f35b --- /dev/null +++ b/examples/view.eve @@ -0,0 +1,203 @@ +# Views + +## Simple + +### Value + +A `#value` view just embeds the `value`(s) its passed. If those values happen to be entities, we'll tag them as such for composite views to mess with. + +~~~ +search @view + wrapper = [#value value] + ix = sort[value] + lookup[record: wrapper, attribute: "tag", value: "value", node] + + is-entity = if substring[text: value to: 1] = "⦑" then true + else false + +bind @browser + wrapper <- [#view #div node class: "view" children: + [#div #value sort: ix text: value is-entity]] +~~~ + +## Tables + +### Table + +A `#table` is a simple N-column N-row grid. + +Create the wrapper for the table. +@NOTE: This needs to sort. + +~~~ +search @view + wrapper = [#table field row] + lookup[record: wrapper, attribute: "tag", value: "table", node] + lookup[record: row attribute: field value] + +bind @browser @view + wrapper <- [#view #table node class: "view" children: + [#thead wrapper sort: 0 children: + [#tr children: + [#td text: field]]] + [#tr row children: + [#td field text: value]]] +~~~ + +### KV Table + +A `#kv-table` is a two-column table which may have many values per key. All values for the same key will be grouped. + +Create the DOM structure for `#kv-table`s. + +~~~ +search @view + wrapper = [#kv-table kvs: [key]] + ix = sort[value: key] + +bind @browser + wrapper <- [#kv-table #view #div class: "view kv-table" children: + [#div #kv-row key class: "kv-row" sort: ix wrapper children: + [#div class: "kv-key" text: key] + [#div #kv-values class: "kv-values" wrapper key]]] +~~~ + +Inject the values for each key in the `#kv-table`. + +~~~ +search @view + wrapper = [#kv-table kvs: [key value]] + ix = sort[value per: key] + +search @browser + value-column = [#kv-values wrapper key] + +bind @view @browser + value-column.children += [#value value-column value sort: ix] +~~~ + +### Attribute Table + +**DEPRECATED** This is a hack, since we do not support dynamic scoping, you cannot control what scope it finds EAVs in. Do not use this. + +~~~ +search @view + wrapper = [#attribute-table entity] + lookup[record: wrapper, attribute: "tag", value: "attribute-table", node] + +search @session @browser + lookup[record: entity attribute: key value] + +bind @view @browser + wrapper.node := node + wrapper.tag += "kv-table" + wrapper.kvs := [key value] +~~~ + + +## Charts & Graphs + +### Bar Graph + +Since we don't have min/max yet, we calculate it separately with sort. + +~~~ +search @view + wrapper = [#bar-graph bar: [height]] + sort[value: height, direction: "down", per: wrapper] = 1 + +bind @view + wrapper.max-height := height +~~~ + +~~~ +search @view + wrapper = [#bar-graph bar max-height] + node = if wrapper.node then wrapper.node + else if lookup[record: wrapper, attribute: "tag", value: "bar-graph", node] then node + + graph-height = if wrapper.height then wrapper.height + else 300 + + graph-width = if wrapper.width then wrapper.width + else 500 + + bar = [label height] + + text = if bar = [#unlabeled] then "" + else label + + sort = if bar.sort then bar.sort + else bar + bar-count = count[given: bar, per: wrapper] + + bar-width = if bar.width then bar.width + else graph-width / bar-count + + bar-height = if max-height = 0 then 0 + else (height / max-height) * (graph-height - 30) // padding! + +bind @browser + wrapper <- [#view #div node class: "view bar-graph" style: [width: graph-width, height: graph-height] children: + [#div wrapper class: "bar-graph-bar" label | sort style: [width: bar-width height: bar-height] text]] +~~~ + +### history + +Commit the values so they stick around + +~~~ +search @view @stored + history = [#history values] + not(values = [#stored]) + values = [value] + ix = if history.ix then history.ix + 1 + else 1 + +commit @stored + history.values += values + values.ix := ix + values += #stored + history.ix := ix +~~~ + +~~~ +search @view + history = [#history values] + values = [value] + +commit @stored + values.value := value +~~~ + +Remove values as the history size grows + +~~~ +search @view @stored + history = [#history values] + size = if history.history-size then history.history-size + else 30 + not(not(values.ix)) + total = count[given: values] + total > size + lowest = min[value: values.ix, given: values] + [#history values: values2] + values2 = [ix: lowest] + +commit @stored + values2 := none +~~~ + +History views are just special bar-graphs + +~~~ +search @view @stored + history = [#history values] + lookup[record: history, attribute: "tag", value: "history", node] + +search @stored + values.value + +bind @view + [#bar-graph history node | bar: [#unlabeled label: values.ix, sort: values.ix, height: values.value width: 15]] +~~~ diff --git a/favicon.png b/favicon.png new file mode 100644 index 000000000..9954640c7 Binary files /dev/null and b/favicon.png differ diff --git a/fonts/ionicons.ttf b/fonts/ionicons.ttf new file mode 100644 index 000000000..180ce515f Binary files /dev/null and b/fonts/ionicons.ttf differ diff --git a/index.html b/index.html new file mode 100644 index 000000000..647d872a9 --- /dev/null +++ b/index.html @@ -0,0 +1,56 @@ + + + Eve + + + + + + + + + + + + + + + + + + + + + diff --git a/package.json b/package.json new file mode 100644 index 000000000..f6810fb59 --- /dev/null +++ b/package.json @@ -0,0 +1,58 @@ +{ + "name": "witheve", + "version": "0.2.2", + "description": "Programming designed for humans", + "keywords": ["language", "ide", "relational", "database", "dataflow"], + "homepage": "http://witheve.com", + "repository": { + "type": "git", + "url": "https://github.com/witheve/Eve" + }, + "bugs": { + "url": "https://github.com/witheve/Eve/issues" + }, + "license": "Apache-2.0", + + "bin": { + "eve": "./bin/eve.js" + }, + + "scripts": { + "build": "./node_modules/.bin/tsc && node ./build/scripts/build.js", + "start": "./node_modules/.bin/tsc && node ./bin/eve.js", + "server": "node ./bin/eve.js", + + "prepublish": "./node_modules/.bin/tsc && node ./build/scripts/build.js", + "build-dist": "node ./build/scripts/build-dist.js", + + "test": "node ./build/test/all.js | faucet" + }, + + "dependencies": { + "@types/body-parser": "0.0.33", + "@types/commonmark": "^0.22.29", + "@types/express": "^4.0.33", + "@types/glob": "^5.0.30", + "@types/minimist": "^1.1.29", + "@types/mkdirp": "^0.3.29", + "@types/node": "^6.0.41", + "@types/request": "0.0.31", + "@types/tape": "^4.2.28", + "@types/ws": "0.0.33", + "body-parser": "^1.15.2", + "chevrotain": "^0.14.0", + "commonmark": "^0.26.0", + "express": "^4.14.0", + "glob": "^7.1.1", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "node-uuid": "^1.4.7", + "request": "^2.75.0", + "typescript": "^2.0.3", + "ws": "^1.1.1" + }, + "devDependencies": { + "faucet": "0.0.1", + "tape": "^4.6.0" + } +} diff --git a/scripts/build-dist.ts b/scripts/build-dist.ts new file mode 100644 index 000000000..18c8f4c6c --- /dev/null +++ b/scripts/build-dist.ts @@ -0,0 +1,56 @@ +import * as fs from "fs"; +import * as glob from "glob"; +import * as mkdirp from "mkdirp"; +import {build, Tracker, copy, onError} from "./build"; + +// Privacy minded? Feel free to flip this off. We just use it to determine anonymous usage patterns to find hangups and unanticipated workflows. +const ENABLE_ANALYTICS = true; +const ANALYTICS_TOKEN = ""; +const ANALYTICS = ` + +`; + +function buildDist(callback:() => void) { + let tracker = new Tracker(callback); + build(() => { + mkdirp.sync("dist/build"); + mkdirp.sync("dist/css"); + + var index = fs.readFileSync("./index.html", "utf-8"); + if(ENABLE_ANALYTICS) { + index = index.replace(ANALYTICS_TOKEN, ANALYTICS); + } + fs.writeFileSync("./dist/index.html", index); + + //copy("./index.html", "./dist/index.html", tracker.track("copy index")); + copy("./build/workspaces.js", "./dist/build/workspaces.js", tracker.track("copy packaged workspaces")); + + + for(let pattern of ["build/src/**/*.js", "build/src/**/*.js.map", "src/**/*.css", "css/**/*.css", "examples/**/*.css"]) { + let matches = glob.sync(pattern); + for(let match of matches) { + let pathname = match.split("/").slice(0, -1).join("/"); + + // @NOTE: Arghhh + mkdirp.sync("dist/" + pathname); + copy(match, "dist/" + match, tracker.track("copy build artifacts")); + } + } + tracker.finishedStartingTasks(); + }); +} + +if(require.main === module) { + console.log("Building distribution folder...") + buildDist(() => { + console.log("done!") + }); +} diff --git a/scripts/build.ts b/scripts/build.ts new file mode 100644 index 000000000..0ad4db47c --- /dev/null +++ b/scripts/build.ts @@ -0,0 +1,91 @@ +import * as path from "path"; +import * as fs from "fs"; +import * as glob from "glob"; +import {packageWorkspaces} from "./package-workspaces"; + +export function onError(err) { + throw err; +} + +export function copy(src, dest, callback) { + let destStream = fs.createWriteStream(dest) + .on("error", onError) + .on("close", callback); + + fs.createReadStream(src) + .on("error", onError) + .pipe(destStream); + + return destStream; +} + +export class Tracker { + inProgress = {}; + protected allTasksStarted = false; + + constructor(public callback:() => void) { } + + finishedStartingTasks() { + this.allTasksStarted = true; + this.checkCompletion(); + } + + checkCompletion() { + if(!this.allTasksStarted) return; + + for(let phase in this.inProgress) { + if(this.inProgress[phase] !== 0) return; + } + this.callback(); + } + + track(phase:string) { + if(!this.inProgress[phase]) { + this.inProgress[phase] = 1; + } else { + this.inProgress[phase] += 1; + } + return () => { + this.inProgress[phase] -= 1; + if(this.inProgress[phase] === 0) console.log(" - " + phase + "... done."); + this.checkCompletion(); + }; + } +} + +// old school +// ./node_modules/.bin/tsc && cp src/*.js build/src/ && cp ./node_modules/chevrotain/lib/chevrotain.js build/src/ && npm run examples + +export function build(callback:() => void) { + let tracker = new Tracker(callback); + + // Copy static JS files into build. + let matches = glob.sync("src/*.js"); + for(let match of matches) { + let relative = match.split("/").slice(1).join("/"); + copy(match, "build/src/" + relative, tracker.track("copy static files")); + } + + // Copy node dependencies required by the browser. + let deps = [ + "node_modules/chevrotain/lib/chevrotain.js" + ]; + for(let dep of deps) { + dep = path.resolve(dep); + let base = dep.split("/").pop(); + copy(dep, "build/src/" + base, tracker.track("copy node module files")); + } + + // Package workspaces. + packageWorkspaces(tracker.track("package workspaces")); + + tracker.finishedStartingTasks(); +} + +if(require.main === module) { + console.log("Building...") + build(() => { + console.log("done.") + console.log("To run eve, type `npm start`"); + }); +} diff --git a/scripts/package-workspaces.ts b/scripts/package-workspaces.ts new file mode 100644 index 000000000..effe7abc9 --- /dev/null +++ b/scripts/package-workspaces.ts @@ -0,0 +1,16 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as eveSource from "../src/runtime/eveSource"; + +eveSource.add("eve", "./examples"); +eveSource.add("examples", "./examples"); + +export function packageWorkspaces(callback:() => void) { + fs.writeFileSync("build/workspaces.js", eveSource.pack()); + callback(); +} + +if(require.main === module) { + console.log("Packaging...") + packageWorkspaces(() => console.log("done.")); +} diff --git a/src/annotatescrollbar.js b/src/annotatescrollbar.js new file mode 100644 index 000000000..dbc09d07c --- /dev/null +++ b/src/annotatescrollbar.js @@ -0,0 +1,120 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + CodeMirror.defineExtension("annotateScrollbar", function(options) { + if (typeof options == "string") options = {className: options}; + return new Annotation(this, options); + }); + + CodeMirror.defineOption("scrollButtonHeight", 0); + + function Annotation(cm, options) { + this.cm = cm; + this.options = options; + this.buttonHeight = options.scrollButtonHeight || cm.getOption("scrollButtonHeight"); + this.annotations = []; + this.doRedraw = this.doUpdate = null; + this.div = cm.getWrapperElement().appendChild(document.createElement("div")); + this.div.style.cssText = "position: absolute; right: 0; top: 0; z-index: 7; pointer-events: none"; + this.computeScale(); + + function scheduleRedraw(delay) { + clearTimeout(self.doRedraw); + self.doRedraw = setTimeout(function() { self.redraw(); }, delay); + } + + var self = this; + cm.on("refresh", this.resizeHandler = function() { + clearTimeout(self.doUpdate); + self.doUpdate = setTimeout(function() { + if (self.computeScale()) scheduleRedraw(20); + }, 100); + }); + cm.on("markerAdded", this.resizeHandler); + cm.on("markerCleared", this.resizeHandler); + if (options.listenForChanges !== false) + cm.on("change", this.changeHandler = function() { + scheduleRedraw(250); + }); + } + + Annotation.prototype.computeScale = function() { + var cm = this.cm; + var hScale = (cm.getWrapperElement().clientHeight - cm.display.barHeight - this.buttonHeight * 2) / + cm.getScrollerElement().scrollHeight + if (hScale != this.hScale) { + this.hScale = hScale; + return true; + } + }; + + Annotation.prototype.update = function(annotations) { + this.annotations = annotations; + this.redraw(); + }; + + Annotation.prototype.redraw = function(compute) { + if (compute !== false) this.computeScale(); + var cm = this.cm, hScale = this.hScale; + + var frag = document.createDocumentFragment(), anns = this.annotations; + + var wrapping = cm.getOption("lineWrapping"); + var singleLineH = wrapping && cm.defaultTextHeight() * 1.5; + var curLine = null, curLineObj = null; + function getY(pos, top) { + if (curLine != pos.line) { + curLine = pos.line; + curLineObj = cm.getLineHandle(curLine); + } + if(!curLineObj) return; + if (wrapping && curLineObj.height > singleLineH) + return cm.charCoords(pos, "local")[top ? "top" : "bottom"]; + var topY = cm.heightAtLine(curLineObj, "local"); + return topY + (top ? 0 : curLineObj.height); + } + + if (cm.display.barWidth) for (var i = 0, nextTop; i < anns.length; i++) { + var ann = anns[i]; + var top = nextTop || getY(ann.from, true) * hScale; + if(!curLineObj) return; + var bottom = getY(ann.to, false) * hScale; + while (i < anns.length - 1) { + nextTop = getY(anns[i + 1].from, true) * hScale; + if (nextTop > bottom + .9) break; + ann = anns[++i]; + bottom = getY(ann.to, false) * hScale; + } + if (bottom == top) continue; + var height = Math.max(bottom - top, 3); + + var elt = frag.appendChild(document.createElement("div")); + elt.style.cssText = "position: absolute; right: 0px; width: " + Math.max(cm.display.barWidth - 1, 2) + "px; top: " + + (top + this.buttonHeight) + "px; height: " + height + "px"; + elt.className = this.options.className; + if (ann.id) { + elt.setAttribute("annotation-id", ann.id); + } + } + this.div.textContent = ""; + this.div.appendChild(frag); + }; + + Annotation.prototype.clear = function() { + this.cm.off("refresh", this.resizeHandler); + this.cm.off("markerAdded", this.resizeHandler); + this.cm.off("markerCleared", this.resizeHandler); + if (this.changeHandler) this.cm.off("change", this.changeHandler); + this.div.parentNode.removeChild(this.div); + }; +}); diff --git a/src/chevrotain.d.ts b/src/chevrotain.d.ts new file mode 100644 index 000000000..f58b349bb --- /dev/null +++ b/src/chevrotain.d.ts @@ -0,0 +1,996 @@ +/*! chevrotain - v0.11.0 */ +declare namespace chevrotain { + class HashTable{} + /** + * This can be used to improve the quality/readability of error messages or syntax diagrams. + * + * @param {Function} clazz - A constructor for a Token subclass + * @returns {string} the Human readable label a Token if it exists. + */ + export function tokenLabel(clazz: Function): string; + export function hasTokenLabel(clazz: Function): boolean; + export function tokenName(clazz: Function): string; + /** + * utility to help the poor souls who are still stuck writing pure javascript 5.1 + * extend and create Token subclasses in a less verbose manner + * + * @param {string} tokenName - the name of the new TokenClass + * @param {RegExp|Function} patternOrParent - RegExp Pattern or Parent Token Constructor + * @param {Function} parentConstructor - the Token class to be extended + * @returns {Function} - a constructor for the new extended Token subclass + */ + export function extendToken(tokenName: string, patternOrParent?: any, parentConstructor?: Function): any; + export class Token { + image: string; + startOffset: number; + endOffset: number; + startLine: number; + startColumn: number; + endLine: number; + endColumn: number; + /** + * A "human readable" Label for a Token. + * Subclasses of Token may define their own static LABEL property. + * This label will be used in error messages and drawing syntax diagrams. + * + * For example a Token constructor may be called LCurly, which is short for LeftCurlyBrackets, These names are either too short + * or too unwieldy to be used in error messages. + * + * Imagine : "expecting LCurly but found ')'" or "expecting LeftCurlyBrackets but found ')'" + * + * However if a static property LABEL with the value '{' exists on LCurly class, that error message will be: + * "expecting '{' but found ')'" + */ + static LABEL: string; + isInsertedInRecovery: boolean; + /** + * @param {string} image the textual representation of the Token as it appeared in the text + * @param {number} offset offset of the first character of the Token + * @param {number} startLine line of the first character of the Token + * @param {number} startColumn column of the first character of the Token + * @param {number} endLine line of the last character of the Token + * @param {number} endColumn column of the last character of the Token + * + * Things to note: + * * "do" {startColumn : 1, endColumn: 2} --> the range is inclusive to exclusive 1...2 (2 chars long). + * * "\n" {startLine : 1, endLine: 1} --> a lineTerminator as the last character does not effect the Token's line numbering. + * * "'hello\tworld\uBBBB'" {image: "'hello\tworld\uBBBB'"} --> a Token's image is the "literal" text + * (unicode escaping is untouched). + */ + constructor(image: string, offset: number, startLine: number, startColumn: number, endLine?: number, endColumn?: number); + } + /** + * a special kind of Token which does not really exist in the input + * (hence the 'Virtual' prefix). These type of Tokens can be used as special markers: + * for example, EOF (end-of-file). + */ + export class VirtualToken extends Token { + constructor(); + } + export class EOF extends VirtualToken { + } + + export type TokenConstructor = Function; + export interface ILexingResult { + tokens: Token[]; + groups: { + [groupName: string]: Token; + }; + errors: ILexingError[]; + } + export enum LexerDefinitionErrorType { + MISSING_PATTERN = 0, + INVALID_PATTERN = 1, + EOI_ANCHOR_FOUND = 2, + UNSUPPORTED_FLAGS_FOUND = 3, + DUPLICATE_PATTERNS_FOUND = 4, + INVALID_GROUP_TYPE_FOUND = 5, + PUSH_MODE_DOES_NOT_EXIST = 6, + MULTI_MODE_LEXER_WITHOUT_DEFAULT_MODE = 7, + MULTI_MODE_LEXER_WITHOUT_MODES_PROPERTY = 8, + MULTI_MODE_LEXER_DEFAULT_MODE_VALUE_DOES_NOT_EXIST = 9, + LEXER_DEFINITION_CANNOT_CONTAIN_UNDEFINED = 10, + } + export interface ILexerDefinitionError { + message: string; + type: LexerDefinitionErrorType; + tokenClasses?: Function[]; + } + export interface ILexingError { + line: number; + column: number; + length: number; + message: string; + } + export type SingleModeLexerDefinition = TokenConstructor[]; + export type MultiModesDefinition = { + [modeName: string]: TokenConstructor[]; + }; + export interface IMultiModeLexerDefinition { + modes: MultiModesDefinition; + defaultMode: string; + } + export class Lexer { + protected lexerDefinition: SingleModeLexerDefinition | IMultiModeLexerDefinition; + static SKIPPED: { + description: string; + }; + static NA: RegExp; + lexerDefinitionErrors: ILexerDefinitionError[]; + protected modes: string[]; + protected defaultMode: string; + protected allPatterns: { + [modeName: string]: RegExp[]; + }; + protected patternIdxToClass: { + [modeName: string]: Function[]; + }; + protected patternIdxToGroup: { + [modeName: string]: string[]; + }; + protected patternIdxToLongerAltIdx: { + [modeName: string]: number[]; + }; + protected patternIdxToCanLineTerminator: { + [modeName: string]: boolean[]; + }; + protected patternIdxToPushMode: { + [modeName: string]: string[]; + }; + protected patternIdxToPopMode: { + [modeName: string]: boolean[]; + }; + protected emptyGroups: { + [groupName: string]: Token; + }; + /** + * @param {SingleModeLexerDefinition | IMultiModeLexerDefinition} lexerDefinition - + * Structure composed of constructor functions for the Tokens types this lexer will support. + * + * In the case of {SingleModeLexerDefinition} the structure is simply an array of Token constructors. + * In the case of {IMultiModeLexerDefinition} the structure is an object with two properties + * 1. a "modes" property where each value is an array of Token. + * 2. a "defaultMode" property specifying the initial lexer mode. + * + * constructors. + * + * for example: + * { + * "modes" : { + * "modeX" : [Token1, Token2] + * "modeY" : [Token3, Token4] + * } + * + * "defaultMode" : "modeY" + * } + * + * A lexer with {MultiModesDefinition} is simply multiple Lexers where only one (mode) can be active at the same time. + * This is useful for lexing languages where there are different lexing rules depending on context. + * + * The current lexing mode is selected via a "mode stack". + * The last (peek) value in the stack will be the current mode of the lexer. + * + * Each Token class can define that it will cause the Lexer to (after consuming an instance of the Token) + * 1. PUSH_MODE : push a new mode to the "mode stack" + * 2. POP_MODE : pop the last mode from the "mode stack" + * + * Examples: + * export class Attribute extends Token { + * static PATTERN = ... + * static PUSH_MODE = "modeY" + * } + * + * export class EndAttribute extends Token { + * static PATTERN = ... + * static POP_MODE = true + * } + * + * The Token constructors must be in one of these forms: + * + * 1. With a PATTERN property that has a RegExp value for tokens to match: + * example: -->class Integer extends Token { static PATTERN = /[1-9]\d }<-- + * + * 2. With a PATTERN property that has the value of the var Lexer.NA defined above. + * This is a convenience form used to avoid matching Token classes that only act as categories. + * example: -->class Keyword extends Token { static PATTERN = NA }<-- + * + * + * The following RegExp patterns are not supported: + * a. '$' for match at end of input + * b. /b global flag + * c. /m multi-line flag + * + * The Lexer will identify the first pattern that matches, Therefor the order of Token Constructors may be significant. + * For example when one pattern may match a prefix of another pattern. + * + * Note that there are situations in which we may wish to order the longer pattern after the shorter one. + * For example: keywords vs Identifiers. + * 'do'(/do/) and 'donald'(/w+) + * + * * If the Identifier pattern appears before the 'do' pattern, both 'do' and 'donald' + * will be lexed as an Identifier. + * + * * If the 'do' pattern appears before the Identifier pattern 'do' will be lexed correctly as a keyword. + * however 'donald' will be lexed as TWO separate tokens: keyword 'do' and identifier 'nald'. + * + * To resolve this problem, add a static property on the keyword's constructor named: LONGER_ALT + * example: + * + * export class Identifier extends Keyword { static PATTERN = /[_a-zA-Z][_a-zA-Z0-9]/ } + * export class Keyword extends Token { + * static PATTERN = lex.NA + * static LONGER_ALT = Identifier + * } + * export class Do extends Keyword { static PATTERN = /do/ } + * export class While extends Keyword { static PATTERN = /while/ } + * export class Return extends Keyword { static PATTERN = /return/ } + * + * The lexer will then also attempt to match a (longer) Identifier each time a keyword is matched. + * + * + * @param {boolean} [deferDefinitionErrorsHandling=false] + * an optional flag indicating that lexer definition errors + * should not automatically cause an error to be raised. + * This can be useful when wishing to indicate lexer errors in another manner + * than simply throwing an error (for example in an online playground). + */ + constructor(lexerDefinition: SingleModeLexerDefinition | IMultiModeLexerDefinition, deferDefinitionErrorsHandling?: boolean); + /** + * Will lex(Tokenize) a string. + * Note that this can be called repeatedly on different strings as this method + * does not modify the state of the Lexer. + * + * @param {string} text - the string to lex + * @param {string} [initialMode] - The initial Lexer Mode to start with, by default this will be the first mode in the lexer's + * definition. If the lexer has no explicit modes it will be the implicit single 'default_mode' mode. + * + * @returns {{tokens: {Token}[], errors: string[]}} + */ + tokenize(text: string, initialMode?: string): ILexingResult; + } + + export enum ParserDefinitionErrorType { + INVALID_RULE_NAME = 0, + DUPLICATE_RULE_NAME = 1, + INVALID_RULE_OVERRIDE = 2, + DUPLICATE_PRODUCTIONS = 3, + UNRESOLVED_SUBRULE_REF = 4, + LEFT_RECURSION = 5, + NONE_LAST_EMPTY_ALT = 6, + AMBIGUOUS_ALTS = 7, + } + export type IgnoredRuleIssues = { + [dslNameAndOccurrence: string]: boolean; + }; + export type IgnoredParserIssues = { + [ruleName: string]: IgnoredRuleIssues; + }; + export interface IParserConfig { + /** + * Is the error recovery / fault tolerance of the Chevrotain Parser enabled. + */ + recoveryEnabled?: boolean; + /** + * Maximum number of tokens the parser will use to choose between alternatives. + */ + maxLookahead?: number; + /** + * Used to mark parser definition errors that should be ignored. + * For example: + * + * { + * myCustomRule : { + * OR3 : true + * }, + * + * myOtherRule : { + * OPTION1 : true, + * OR4 : true + * } + * } + * + * Be careful when ignoring errors, they are usually there for a reason :). + */ + ignoredIssues?: IgnoredParserIssues; + } + export interface IRuleConfig { + /** + * The function which will be invoked to produce the returned value for a production that have not been + * successfully executed and the parser recovered from. + */ + recoveryValueFunc?: () => T; + /** + * Enable/Disable re-sync error recovery for this specific production. + */ + resyncEnabled?: boolean; + } + export interface IParserDefinitionError { + message: string; + type: ParserDefinitionErrorType; + ruleName: string; + } + export interface IParserDuplicatesDefinitionError extends IParserDefinitionError { + dslName: string; + occurrence: number; + parameter?: string; + } + export interface IParserEmptyAlternativeDefinitionError extends IParserDefinitionError { + occurrence: number; + alternative: number; + } + export interface IParserAmbiguousAlternativesDefinitionError extends IParserDefinitionError { + occurrence: number; + alternatives: number[]; + } + export interface IParserUnresolvedRefDefinitionError extends IParserDefinitionError { + unresolvedRefName: string; + } + export interface IFollowKey { + ruleName: string; + idxInCallingRule: number; + inRule: string; + } + /** + * OR([ + * { WHEN:LA1, THEN_DO:XXX }, + * { WHEN:LA2, THEN_DO:YYY }, + * { WHEN:LA3, THEN_DO:ZZZ }, + * ]) + */ + export interface IOrAlt { + WHEN: () => boolean; + THEN_DO: () => T; + } + /** + * OR([ + * {ALT:XXX }, + * {ALT:YYY }, + * {ALT:ZZZ } + * ]) + */ + export interface IOrAltWithPredicate { + ALT: () => T; + } + export type IAnyOrAlt = IOrAlt | IOrAltWithPredicate; + export interface IParserState { + errors: exceptions.IRecognitionException[]; + inputIdx: number; + RULE_STACK: string[]; + } + export type Predicate = () => boolean; + export type GrammarAction = () => void; + /** + * Convenience used to express an empty alternative in an OR (alternation). + * can be used to more clearly describe the intent in a case of empty alternation. + * + * For example: + * + * 1. without using EMPTY_ALT: + * + * this.OR([ + * {ALT: () => { + * this.CONSUME1(OneTok) + * return "1" + * }}, + * {ALT: () => { + * this.CONSUME1(TwoTok) + * return "2" + * }}, + * {ALT: () => { // implicitly empty because there are no invoked grammar rules (OR/MANY/CONSUME...) inside this alternative. + * return "666" + * }}, + * ]) + * + * + * 2. using EMPTY_ALT: + * + * this.OR([ + * {ALT: () => { + * this.CONSUME1(OneTok) + * return "1" + * }}, + * {ALT: () => { + * this.CONSUME1(TwoTok) + * return "2" + * }}, + * {ALT: EMPTY_ALT("666")}, // explicitly empty, clearer intent + * ]) + * + */ + export function EMPTY_ALT(value?: T): () => T; + /** + * A Recognizer capable of self analysis to determine it's grammar structure + * This is used for more advanced features requiring such information. + * for example: Error Recovery, Automatic lookahead calculation + */ + export class Parser { + static NO_RESYNC: boolean; + static DEFER_DEFINITION_ERRORS_HANDLING: boolean; + protected static performSelfAnalysis(parserInstance: Parser): void; + errors: exceptions.IRecognitionException[]; + /** + * This flag enables or disables error recovery (fault tolerance) of the parser. + * If this flag is disabled the parser will halt on the first error. + */ + protected recoveryEnabled: boolean; + protected maxLookahead: number; + protected ignoredIssues: IgnoredParserIssues; + protected _input: Token[]; + protected inputIdx: number; + protected isBackTrackingStack: any[]; + protected className: string; + protected RULE_STACK: string[]; + protected RULE_OCCURRENCE_STACK: number[]; + protected tokensMap: { + [fqn: string]: Function; + }; + /** + * Only used internally for storing productions as they are built for the first time. + * The final productions should be accessed from the static cache. + */ + constructor(input: Token[], tokensMapOrArr: { + [fqn: string]: Function; + } | Function[], config?: IParserConfig); + input: Token[]; + reset(): void; + isAtEndOfInput(): boolean; + getGAstProductions(): HashTable; + protected isBackTracking(): boolean; + protected SAVE_ERROR(error: exceptions.IRecognitionException): exceptions.IRecognitionException; + protected NEXT_TOKEN(): Token; + protected LA(howMuch: number): Token; + /** + * @param grammarRule - the rule to try and parse in backtracking mode + * @param isValid - a predicate that given the result of the parse attempt will "decide" if the parse was successfully or not + * + * @return a lookahead function that will try to parse the given grammarRule and will return true if succeed + */ + protected BACKTRACK(grammarRule: (...args) => T, isValid: (T) => boolean): () => boolean; + protected SKIP_TOKEN(): Token; + /** + * Convenience method equivalent to CONSUME1 + * @see CONSUME1 + */ + protected CONSUME(tokClass: Function): Token; + /** + * + * A Parsing DSL method use to consume a single terminal Token. + * a Token will be consumed, IFF the next token in the token vector is an instanceof tokClass. + * otherwise the parser will attempt to perform error recovery. + * + * The index in the method name indicates the unique occurrence of a terminal consumption + * inside a the top level rule. What this means is that if a terminal appears + * more than once in a single rule, each appearance must have a difference index. + * + * for example: + * + * function parseQualifiedName() { + * this.CONSUME1(Identifier); + * this.MANY(()=> { + * this.CONSUME1(Dot); + * this.CONSUME2(Identifier); // <-- here we use CONSUME2 because the terminal + * }); // 'Identifier' has already appeared previously in the + * // the rule 'parseQualifiedName' + * } + * + * @param {Function} tokClass - A constructor function specifying the type of token to be consumed. + * + * @returns {Token} The consumed token. + */ + protected CONSUME1(tokClass: Function): Token; + /** + * @see CONSUME1 + */ + protected CONSUME2(tokClass: Function): Token; + /** + * @see CONSUME1 + */ + protected CONSUME3(tokClass: Function): Token; + /** + * @see CONSUME1 + */ + protected CONSUME4(tokClass: Function): Token; + /** + * @see CONSUME1 + */ + protected CONSUME5(tokClass: Function): Token; + /** + * Convenience method equivalent to SUBRULE1 + * @see SUBRULE1 + */ + protected SUBRULE(ruleToCall: (number) => T, args?: any[]): T; + /** + * The Parsing DSL Method is used by one rule to call another. + * + * This may seem redundant as it does not actually do much. + * However using it is mandatory for all sub rule invocations. + * calling another rule without wrapping in SUBRULE(...) + * will cause errors/mistakes in the Recognizer's self analysis + * which will lead to errors in error recovery/automatic lookahead calculation + * and any other functionality relying on the Recognizer's self analysis + * output. + * + * As in CONSUME the index in the method name indicates the occurrence + * of the sub rule invocation in its rule. + * + * @param {Function} ruleToCall - the rule to invoke + * @param {*[]} args - the arguments to pass to the invoked subrule + * @returns {*} the result of invoking ruleToCall + */ + protected SUBRULE1(ruleToCall: (number) => T, args?: any[]): T; + /** + * @see SUBRULE1 + */ + protected SUBRULE2(ruleToCall: (number) => T, args?: any[]): T; + /** + * @see SUBRULE1 + */ + protected SUBRULE3(ruleToCall: (number) => T, args?: any[]): T; + /** + * @see SUBRULE1 + */ + protected SUBRULE4(ruleToCall: (number) => T, args?: any[]): T; + /** + * @see SUBRULE1 + */ + protected SUBRULE5(ruleToCall: (number) => T, args?: any[]): T; + /** + * Convenience method equivalent to OPTION1 + * @see OPTION1 + */ + protected OPTION(predicateOrAction: Predicate | GrammarAction, action?: GrammarAction): boolean; + /** + * Parsing DSL Method that Indicates an Optional production + * in EBNF notation: [...] + * + * note that the 'action' param is optional. so both of the following forms are valid: + * + * short: this.OPTION(()=>{ this.CONSUME(Digit}); + * long: this.OPTION(predicateFunc, ()=>{ this.CONSUME(Digit}); + * + * The 'predicateFunc' in the long form can be used to add constraints (none grammar related) + * to optionally invoking the grammar action. + * + * As in CONSUME the index in the method name indicates the occurrence + * of the optional production in it's top rule. + * + * @param {Function} predicateOrAction - The predicate / gate function that implements the constraint on the grammar + * or the grammar action to optionally invoke once. + * @param {Function} [action] - The action to optionally invoke. + * + * @returns {boolean} true iff the OPTION's action has been invoked + */ + protected OPTION1(predicateOrAction: Predicate | GrammarAction, action?: GrammarAction): boolean; + /** + * @see OPTION1 + */ + protected OPTION2(predicateOrAction: Predicate | GrammarAction, action?: GrammarAction): boolean; + /** + * @see OPTION1 + */ + protected OPTION3(predicateOrAction: Predicate | GrammarAction, action?: GrammarAction): boolean; + /** + * @see OPTION1 + */ + protected OPTION4(predicateOrAction: Predicate | GrammarAction, action?: GrammarAction): boolean; + /** + * @see OPTION1 + */ + protected OPTION5(predicateOrAction: Predicate | GrammarAction, action?: GrammarAction): boolean; + /** + * Convenience method equivalent to OR1 + * @see OR1 + */ + protected OR(alts: IAnyOrAlt[], errMsgTypes?: string): T; + /** + * Parsing DSL method that indicates a choice between a set of alternatives must be made. + * This is equivalent to EBNF alternation (A | B | C | D ...) + * + * There are two forms: + * + * short: this.OR([ + * {ALT:()=>{this.CONSUME(One)}}, + * {ALT:()=>{this.CONSUME(Two)}}, + * {ALT:()=>{this.CONSUME(Three)}}, + * ], "a number") + * + * long: this.OR([ + * {WHEN: predicateFunc1, THEN_DO:()=>{this.CONSUME(One)}}, + * {WHEN: predicateFuncX, THEN_DO:()=>{this.CONSUME(Two)}}, + * {WHEN: predicateFuncX, THEN_DO:()=>{this.CONSUME(Three)}}, + * ], "a number") + * + * They can also be mixed: + * mixed: this.OR([ + * {WHEN: predicateFunc1, THEN_DO:()=>{this.CONSUME(One)}}, + * {ALT:()=>{this.CONSUME(Two)}}, + * {ALT:()=>{this.CONSUME(Three)}} + * ], "a number") + * + * The 'predicateFuncX' in the long form can be used to add constraints (none grammar related) to choosing the alternative. + * + * As in CONSUME the index in the method name indicates the occurrence + * of the alternation production in it's top rule. + * + * @param {{ALT:Function}[] | {WHEN:Function, THEN_DO:Function}[]} alts - An array of alternatives + * + * @param {string} [errMsgTypes] - A description for the alternatives used in error messages + * If none is provided, the error message will include the names of the expected + * Tokens sequences which may start each alternative. + * + * @returns {*} The result of invoking the chosen alternative + */ + protected OR1(alts: IAnyOrAlt[], errMsgTypes?: string): T; + /** + * @see OR1 + */ + protected OR2(alts: IAnyOrAlt[], errMsgTypes?: string): T; + /** + * @see OR1 + */ + protected OR3(alts: IAnyOrAlt[], errMsgTypes?: string): T; + /** + * @see OR1 + */ + protected OR4(alts: IAnyOrAlt[], errMsgTypes?: string): T; + /** + * @see OR1 + */ + protected OR5(alts: IAnyOrAlt[], errMsgTypes?: string): T; + /** + * Convenience method equivalent to MANY1 + * @see MANY1 + */ + protected MANY(predicateOrAction: Predicate | GrammarAction, action?: GrammarAction): void; + /** + * Parsing DSL method, that indicates a repetition of zero or more. + * This is equivalent to EBNF repetition {...} + * + * note that the 'action' param is optional. so both of the following forms are valid: + * + * short: this.MANY(()=>{ + * this.CONSUME(Comma}; + * this.CONSUME(Digit}); + * + * long: this.MANY(predicateFunc, () => { + * this.CONSUME(Comma}; + * this.CONSUME(Digit}); + * + * The 'predicateFunc' in the long form can be used to add constraints (none grammar related) taking another iteration. + * + * As in CONSUME the index in the method name indicates the occurrence + * of the repetition production in it's top rule. + * + * @param {Function} predicateOrAction - The predicate / gate function that implements the constraint on the grammar + * or the grammar action to optionally invoke multiple times. + * @param {Function} [action] - The action to optionally invoke multiple times. + */ + protected MANY1(predicateOrAction: Predicate | GrammarAction, action?: GrammarAction): void; + /** + * @see MANY1 + */ + protected MANY2(predicateOrAction: Predicate | GrammarAction, action?: GrammarAction): void; + /** + * @see MANY1 + */ + protected MANY3(predicateOrAction: Predicate | GrammarAction, action?: GrammarAction): void; + /** + * @see MANY1 + */ + protected MANY4(predicateOrAction: Predicate | GrammarAction, action?: GrammarAction): void; + /** + * @see MANY1 + */ + protected MANY5(predicateOrAction: Predicate | GrammarAction, action?: GrammarAction): void; + /** + * Convenience method equivalent to MANY_SEP1 + * @see MANY_SEP1 + */ + protected MANY_SEP(separator: TokenConstructor, action: GrammarAction): Token[]; + /** + * Parsing DSL method, that indicates a repetition of zero or more with a separator + * Token between the repetitions. + * + * Example: + * + * this.MANY_SEP(Comma, () => { + * this.CONSUME(Number}; + * ... + * ); + * + * Note that for the purposes of deciding on whether or not another iteration exists + * Only a single Token is examined (The separator). Therefore if the grammar being implemented is + * so "crazy" to require multiple tokens to identify an item separator please use the basic DSL methods + * to implement it. + * + * As in CONSUME the index in the method name indicates the occurrence + * of the repetition production in it's top rule. + * + * @param {TokenConstructor} separator - The Token class which will be used as a separator between repetitions. + * @param {Function} [action] - The action to optionally invoke. + * + * @return {Token[]} - The consumed separator Tokens. + */ + protected MANY_SEP1(separator: TokenConstructor, action: GrammarAction): Token[]; + /** + * @see MANY_SEP1 + */ + protected MANY_SEP2(separator: TokenConstructor, action: GrammarAction): Token[]; + /** + * @see MANY_SEP1 + */ + protected MANY_SEP3(separator: TokenConstructor, action: GrammarAction): Token[]; + /** + * @see MANY_SEP1 + */ + protected MANY_SEP4(separator: TokenConstructor, action: GrammarAction): Token[]; + /** + * @see MANY_SEP1 + */ + protected MANY_SEP5(separator: TokenConstructor, action: GrammarAction): Token[]; + /** + * Convenience method equivalent to AT_LEAST_ONE1 + * @see AT_LEAST_ONE1 + */ + protected AT_LEAST_ONE(predicateOrAction: Predicate | GrammarAction, action?: GrammarAction | string, errMsg?: string): void; + /** + * + * convenience method, same as MANY but the repetition is of one or more. + * failing to match at least one repetition will result in a parsing error and + * cause the parser to attempt error recovery. + * + * @see MANY1 + * + * @param {Function} predicateOrAction - The predicate / gate function that implements the constraint on the grammar + * or the grammar action to invoke at least once. + * @param {Function} [action] - The action to optionally invoke. + * @param {string} [errMsg] short title/classification to what is being matched + */ + protected AT_LEAST_ONE1(predicateOrAction: Predicate | GrammarAction, action?: GrammarAction | string, errMsg?: string): void; + /** + * @see AT_LEAST_ONE1 + */ + protected AT_LEAST_ONE2(predicateOrAction: Predicate | GrammarAction, action?: GrammarAction | string, errMsg?: string): void; + /** + * @see AT_LEAST_ONE1 + */ + protected AT_LEAST_ONE3(predicateOrAction: Predicate | GrammarAction, action?: GrammarAction | string, errMsg?: string): void; + /** + * @see AT_LEAST_ONE1 + */ + protected AT_LEAST_ONE4(predicateOrAction: Predicate | GrammarAction, action?: GrammarAction | string, errMsg?: string): void; + /** + * @see AT_LEAST_ONE1 + */ + protected AT_LEAST_ONE5(predicateOrAction: Predicate | GrammarAction, action?: GrammarAction | string, errMsg?: string): void; + /** + * Convenience method equivalent to AT_LEAST_ONE_SEP1 + * @see AT_LEAST_ONE1 + */ + protected AT_LEAST_ONE_SEP(separator: TokenConstructor, action: GrammarAction | string, errMsg?: string): Token[]; + /** + * + * convenience method, same as MANY_SEP but the repetition is of one or more. + * failing to match at least one repetition will result in a parsing error and + * cause the parser to attempt error recovery. + * + * @see MANY_SEP1 + * + * @param {TokenConstructor} separator - The Token class which will be used as a separator between repetitions. + * @param {Function} [action] - The action to optionally invoke. + * @param {string} [errMsg] - short title/classification to what is being matched + */ + protected AT_LEAST_ONE_SEP1(separator: TokenConstructor, action: GrammarAction | string, errMsg?: string): Token[]; + /** + * @see AT_LEAST_ONE_SEP1 + */ + protected AT_LEAST_ONE_SEP2(separator: TokenConstructor, action: GrammarAction | string, errMsg?: string): Token[]; + /** + * @see AT_LEAST_ONE_SEP1 + */ + protected AT_LEAST_ONE_SEP3(separator: TokenConstructor, action: GrammarAction | string, errMsg?: string): Token[]; + /** + * @see AT_LEAST_ONE_SEP1 + */ + protected AT_LEAST_ONE_SEP4(separator: TokenConstructor, action: GrammarAction | string, errMsg?: string): Token[]; + /** + * @see AT_LEAST_ONE_SEP1 + */ + protected AT_LEAST_ONE_SEP5(separator: TokenConstructor, action: GrammarAction | string, errMsg?: string): Token[]; + /** + * + * @param {string} name - The name of the rule. + * @param {Function} implementation - The implementation of the rule. + * @param {IRuleConfig} [config] - The rule's optional configuration + * + * @returns {Function} The parsing rule which is the production implementation wrapped with the parsing logic that handles + * Parser state / error recovery&reporting/ ... + */ + protected RULE(name: string, implementation: (...implArgs: any[]) => T, config?: IRuleConfig): (idxInCallingRule?: number, ...args: any[]) => T; + /** + * @See RULE + * same as RULE, but should only be used in "extending" grammars to override rules/productions + * from the super grammar. + */ + protected OVERRIDE_RULE(name: string, impl: (...implArgs: any[]) => T, config?: IRuleConfig): (idxInCallingRule?: number, ...args: any[]) => T; + protected ruleInvocationStateUpdate(ruleName: string, idxInCallingRule: number): void; + protected ruleFinallyStateUpdate(): void; + /** + * Returns an "imaginary" Token to insert when Single Token Insertion is done + * Override this if you require special behavior in your grammar + * for example if an IntegerToken is required provide one with the image '0' so it would be valid syntactically + */ + protected getTokenToInsert(tokClass: Function): Token; + /** + * By default all tokens type may be inserted. This behavior may be overridden in inheriting Recognizers + * for example: One may decide that only punctuation tokens may be inserted automatically as they have no additional + * semantic value. (A mandatory semicolon has no additional semantic meaning, but an Integer may have additional meaning + * depending on its int value and context (Inserting an integer 0 in cardinality: "[1..]" will cause semantic issues + * as the max of the cardinality will be greater than the min value. (and this is a false error!) + */ + protected canTokenTypeBeInsertedInRecovery(tokClass: Function): boolean; + /** + * @param {Token} actualToken - The actual unexpected (mismatched) Token instance encountered. + * @param {Function} expectedTokType - The Class of the expected Token. + * @returns {string} The error message saved as part of a MismatchedTokenException. + */ + protected getMisMatchTokenErrorMessage(expectedTokType: Function, actualToken: Token): string; + protected getCurrentGrammarPath(tokClass: Function, tokIdxInRule: number): ITokenGrammarPath; + protected getNextPossibleTokenTypes(grammarPath: ITokenGrammarPath): Function[]; + /** + * @param tokClass - The Type of Token we wish to consume (Reference to its constructor function) + * @param idx - occurrence index of consumed token in the invoking parser rule text + * for example: + * IDENT (DOT IDENT)* + * the first ident will have idx 1 and the second one idx 2 + * * note that for the second ident the idx is always 2 even if its invoked 30 times in the same rule + * the idx is about the position in grammar (source code) and has nothing to do with a specific invocation + * details + * + * @returns the consumed Token + */ + protected consumeInternal(tokClass: Function, idx: number): Token; + } + + export namespace exceptions { + interface IRecognizerContext { + /** + * A copy of the parser's rule stack at the "time" the RecognitionException occurred. + * This can be used to help debug parsing errors (How did we get here?) + */ + ruleStack: string[]; + /** + * A copy of the parser's rule occurrence stack at the "time" the RecognitionException occurred. + * This can be used to help debug parsing errors (How did we get here?) + */ + ruleOccurrenceStack: number[]; + } + interface IRecognitionException { + name: string; + message: string; + /** + * The token which caused the parser error. + */ + token: Token; + /** + * Additional tokens which have been re-synced in error recovery due to the original error. + * This information can be used the calculate the whole text area which has been skipped due to an error. + * For example for displaying with a red underline in a text editor. + */ + resyncedTokens: Token[]; + context: IRecognizerContext; + } + function isRecognitionException(error: Error): boolean; + function MismatchedTokenException(message: string, token: Token): void; + function NoViableAltException(message: string, token: Token): void; + function NotAllInputParsedException(message: string, token: Token): void; + function EarlyExitException(message: string, token: Token): void; + } + + /** + * this interfaces defines the path the parser "took" to reach a certain position + * in the grammar. + */ + export interface IGrammarPath { + ruleStack: string[]; + occurrenceStack: number[]; + } + export interface ITokenGrammarPath extends IGrammarPath { + lastTok: Function; + lastTokOccurrence: number; + } + export interface IRuleGrammarPath extends IGrammarPath { + occurrence: number; + } + + export namespace gast { + interface IProduction { + accept(visitor: GAstVisitor): void; + } + interface IProductionWithOccurrence extends IProduction { + occurrenceInParent: number; + implicitOccurrenceIndex: boolean; + } + abstract class AbstractProduction implements IProduction { + definition: IProduction[]; + implicitOccurrenceIndex: boolean; + constructor(definition: IProduction[]); + accept(visitor: GAstVisitor): void; + } + class NonTerminal extends AbstractProduction implements IProductionWithOccurrence { + nonTerminalName: string; + referencedRule: Rule; + occurrenceInParent: number; + constructor(nonTerminalName: string, referencedRule?: Rule, occurrenceInParent?: number); + definition: IProduction[]; + accept(visitor: GAstVisitor): void; + } + class Rule extends AbstractProduction { + name: string; + orgText: string; + constructor(name: string, definition: IProduction[], orgText?: string); + } + class Flat extends AbstractProduction { + constructor(definition: IProduction[]); + } + class Option extends AbstractProduction implements IProductionWithOccurrence { + occurrenceInParent: number; + constructor(definition: IProduction[], occurrenceInParent?: number); + } + class RepetitionMandatory extends AbstractProduction implements IProductionWithOccurrence { + occurrenceInParent: number; + constructor(definition: IProduction[], occurrenceInParent?: number); + } + class RepetitionMandatoryWithSeparator extends AbstractProduction implements IProductionWithOccurrence { + separator: Function; + occurrenceInParent: number; + constructor(definition: IProduction[], separator: Function, occurrenceInParent?: number); + } + class Repetition extends AbstractProduction implements IProductionWithOccurrence { + occurrenceInParent: number; + constructor(definition: IProduction[], occurrenceInParent?: number); + } + class RepetitionWithSeparator extends AbstractProduction implements IProductionWithOccurrence { + separator: Function; + occurrenceInParent: number; + constructor(definition: IProduction[], separator: Function, occurrenceInParent?: number); + } + class Alternation extends AbstractProduction implements IProductionWithOccurrence { + occurrenceInParent: number; + constructor(definition: Flat[], occurrenceInParent?: number); + } + class Terminal implements IProductionWithOccurrence { + terminalType: Function; + occurrenceInParent: number; + implicitOccurrenceIndex: boolean; + constructor(terminalType: Function, occurrenceInParent?: number); + accept(visitor: GAstVisitor): void; + } + abstract class GAstVisitor { + visit(node: IProduction): any; + visitNonTerminal(node: NonTerminal): any; + visitFlat(node: Flat): any; + visitOption(node: Option): any; + visitRepetition(node: Repetition): any; + visitRepetitionMandatory(node: RepetitionMandatory): any; + visitRepetitionMandatoryWithSeparator(node: RepetitionMandatoryWithSeparator): any; + visitRepetitionWithSeparator(node: RepetitionWithSeparator): any; + visitAlternation(node: Alternation): any; + visitTerminal(node: Terminal): any; + visitRule(node: Rule): any; + } + } + + /** + * Clears the chevrotain internal cache. + * This should not be used in regular work flows, This is intended for + * unique use cases for example: online playground where the a parser with the same name is initialized with + * different implementations multiple times. + */ + export function clearCache(): void; + +} + +declare module "chevrotain" { + export = chevrotain; +} diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 000000000..c648989b2 --- /dev/null +++ b/src/client.ts @@ -0,0 +1,519 @@ +import {clone, debounce, uuid, sortComparator} from "./util"; +import {Owner} from "./config"; +import {sentInputValues, activeIds, renderRecords, renderEve} from "./renderer" +import {IDE} from "./ide"; +import * as browser from "./runtime/browser"; + +import {IndexScalar, IndexList, EAV, Record} from "./db" + + +function analyticsEvent(kind: string, label?: string, value?: number) { + let ga = window["ga"]; + if(!ga) return; + + ga("send", "event", "ide", kind, label, value); +} + +//--------------------------------------------------------- +// Connect the websocket, send the ui code +//--------------------------------------------------------- + +export var DEBUG:string|boolean = false; + +export var indexes = { + records: new IndexScalar(), // E -> Record + dirty: new IndexList(), // E -> A + byName: new IndexList(), // name -> E + byTag: new IndexList(), // tag -> E + + // renderer indexes + byClass: new IndexList(), // class -> E + byStyle: new IndexList(), // style -> E + byChild: new IndexScalar() // child -> E +}; + +function handleDiff(state, diff) { + let diffEntities = 0; + let entitiesWithUpdatedValues = {}; + + let records = indexes.records; + let dirty = indexes.dirty; + + for(let remove of diff.remove) { + let [e, a, v] = remove; + if(!records.index[e]) { + console.error(`Attempting to remove an attribute of an entity that doesn't exist: ${e}`); + continue; + } + + let entity = records.index[e]; + let values = entity[a]; + if(!values) continue; + dirty.insert(e, a); + + if(values.length <= 1 && values[0] === v) { + delete entity[a]; + } else { + let ix = values.indexOf(v); + if(ix === -1) continue; + values.splice(ix, 1); + } + + // Update indexes + if(a === "tag") indexes.byTag.remove(v, e); + else if(a === "name") indexes.byName.remove(v, e); + else if(a === "class") indexes.byClass.remove(v, e); + else if(a === "style") indexes.byStyle.remove(v, e); + // @NOTE: We intentionally leak children -> parent for now to easily restore + // children that get recreated with the same id which don't have an associated diff in their parent. + //else if(a === "children") indexes.byChild.remove(v, e); + else if(a === "value") entitiesWithUpdatedValues[e] = true; + + } + + for(let insert of diff.insert) { + let [e, a, v] = insert; + let entity = records.index[e]; + if(!entity) { + entity = {}; + records.insert(e, entity); + diffEntities++; // Nuke this and use records.dirty + } + + dirty.insert(e, a); + + if(!entity[a]) entity[a] = []; + entity[a].push(v); + + // Update indexes + if(a === "tag") indexes.byTag.insert(v, e); + else if(a === "name") indexes.byName.insert(v, e); + else if(a === "class") indexes.byClass.insert(v, e); + else if(a === "style") indexes.byStyle.insert(v, e); + else if(a === "children") indexes.byChild.insert(v, e); + else if(a === "value") entitiesWithUpdatedValues[e] = true; + } + + // Update value syncing + for(let e in entitiesWithUpdatedValues) { + let a = "value"; + let entity = records.index[e]; + if(!entity[a]) { + sentInputValues[e] = []; + } else { + if(entity[a].length > 1) console.error("Unable to set 'value' multiple times on entity", e, entity[a]); + let value = entity[a][0]; + let sent = sentInputValues[e]; + if(sent && sent[0] === value) { + dirty.remove(e, a); + sent.shift(); + } else { + sentInputValues[e] = []; + } + } + } + // Trigger all the subscribers of dirty indexes + for(let indexName in indexes) { + indexes[indexName].dispatchIfDirty(); + } + // Clear dirty states afterwards so a subscriber of X can see the dirty state of Y reliably + for(let indexName in indexes) { + indexes[indexName].clearDirty(); + } + // Finally, wipe the dirty E -> A index + indexes.dirty.clearIndex(); +} + +let prerendering = false; +var frameRequested = false; + +//--------------------------------------------------------- +// EveClient +//--------------------------------------------------------- + +export class EveClient { + socket: WebSocket; + socketQueue: string[] = []; + localEve:boolean = false; + localControl:boolean = false; + showIDE:boolean = true; + ide:IDE; + + constructor(url?:string) { + let loc = url ? url : this.getUrl(); + + this.socket = new WebSocket(loc); + this.socket.onerror = (event) => { + this.onError(); + } + this.socket.onopen = (event) => { + this.onOpen(); + } + this.socket.onmessage = (event) => { + this.onMessage(event); + } + this.socket.onclose = (event) => { + this.onClose(); + } + } + + getUrl() { + let protocol = "ws://"; + if(location.protocol.indexOf("https") > -1) { + protocol = "wss://"; + } + return protocol + window.location.host +"/ws"; + } + + socketSend(message:string) { + if(this.socket && this.socket.readyState === 1) { + this.socket.send(message); + } else { + this.socketQueue.push(message); + } + } + + send(payload:{type: string, [attributes:string]: any}) { + let message = JSON.stringify(payload); + if(!this.localEve) { + this.socketSend(message); + } else { + browser.responder.handleEvent(message); + } + } + + sendControl(message:string) { + if(!this.localControl) { + this.socketSend(message); + } else { + // @TODO where do local control messages go? + } + } + + sendEvent(records:any[]) { + if(!records || !records.length) return; + let eavs = []; + for(let record of records) { + eavs.push.apply(eavs, recordToEAVs(record)); + } + this.send({type: "event", insert: eavs}) + } + + onError() { + this.localControl = true; + this.localEve = true; + if(!this.ide) { + this._initProgram({runtimeOwner: Owner.client, controlOwner: Owner.client, withIDE: true, path: (window.location.hash || "").slice(1) || "/examples/quickstart.eve"}); + } else if(this.showIDE) { + this.ide.injectNotice("error", "Unexpectedly disconnected from the server. Please refresh the page."); + } else { + console.error("Unexpectedly disconnected from the server. Please refresh the page."); + } + } + + onOpen() { + this.socketSend(JSON.stringify({type: "init", url: location.pathname, hash: location.hash.substring(1)})) + for(let queued of this.socketQueue) { + this.socketSend(queued); + } + // ping the server so that the connection isn't overzealously + // closed + setInterval(() => { + this.socketSend(JSON.stringify({type: "ping"})); + }, 30000); + } + + onClose() { + this.ide.injectNotice("warning", "The editor has lost connection to the Eve server. All changes will be made locally."); + } + + onMessage(event) { + let data = JSON.parse(event.data); + let handler = this["_" + data.type]; + if(handler) { + handler.call(this, data); + } else if(!this.ide || !this.ide.languageService.handleMessage(data)) { + console.error(`Unknown client message type: ${data.type}`); + } + } + + _result(data) { + let state = {entities: indexes.records.index, dirty: indexes.dirty.index}; + handleDiff(state, data); + + let diffEntities = 0; + if(DEBUG) { + console.groupCollapsed(`Received Result +${data.insert.length}/-${data.remove.length} (∂Entities: ${diffEntities})`); + if(DEBUG === true || DEBUG === "diff") { + console.table(data.insert); + console.table(data.remove); + } + if(DEBUG === true || DEBUG === "state") { + // we clone here to keep the entities fresh when you want to thumb through them in the log later (since they are rendered lazily) + let copy = clone(state.entities); + + console.info("Entities", copy); + console.info("Indexes", indexes); + } + console.groupEnd(); + } + + if(document.readyState === "complete") { + renderEve(); + } else if(!prerendering) { + prerendering = true; + document.addEventListener("DOMContentLoaded", function() { + renderEve(); + }); + } + } + + _initProgram(data) { + this.localEve = data.runtimeOwner === Owner.client; + this.localControl = data.controlOwner === Owner.client; + this.showIDE = data.withIDE; + if(this.localEve) { + browser.init(data.code); + } + if(this.showIDE) { + // Ensure the URL bar is in sync with the server. + // @FIXME: This back and forth of control over where we are + // is an Escherian nightmare. + if(!data.path) { + history.pushState({}, "", window.location.pathname); + } + + this.ide = new IDE(); + this.ide.local = this.localControl; + initIDE(this); + this.ide.render(); + if(data.path && data.path.length > 2) { + this.ide.loadFile(data.path, data.code); + } + } + onHashChange({}); + } + + _parse(data) { + if(!this.showIDE) return; + this.ide.loadDocument(data.generation, data.text, data.spans, data.extraInfo); // @FIXME + } + + _comments(data) { + if(!this.showIDE) return; + this.ide.injectSpans(data.spans, data.extraInfo); + } + + _findNode(data) { + if(!this.showIDE) return; + this.ide.attachView(data.recordId, data.spanId); + } + + _error(data) { + if(!this.showIDE) return; + this.ide.injectNotice("error", data.message); + } + +} + +//--------------------------------------------------------- +// create socket +//--------------------------------------------------------- + +// @FIXME: This is just so bad. +// We'll create the socket at the end to kick off this whole ball of earwax and nail clippings. +export var socket; + +//--------------------------------------------------------- +// Index handlers +//--------------------------------------------------------- + +function renderOnChange(index, dirty) { + renderRecords(); +} +indexes.dirty.subscribe(renderOnChange); + +function printDebugRecords(index, dirty) { + for(let recordId in dirty) { + let record = indexes.records.index[recordId]; + if(record.tag && record.tag.indexOf("debug") !== -1) { + console.info(record); + } + } +} +indexes.dirty.subscribe(printDebugRecords); + +function subscribeToTagDiff(tag:string, callback: (inserts: string[], removes: string[], records: {[recordId:string]: any}) => void) { + indexes.dirty.subscribe((index, dirty) => { + let records = {}; + let inserts = []; + let removes = []; + + let dirtyOldRecords = indexes.byTag.dirty[tag] || []; + for(let recordId of dirtyOldRecords) { + let record = indexes.records.index[recordId]; + if(!record || !record.tag || record.tag.indexOf(tag) === -1) { + removes.push(recordId); + } + } + + for(let recordId in dirty) { + let record = indexes.records.index[recordId]; + if(record.tag && record.tag.indexOf(tag) !== -1) { + inserts.push(recordId); + records[recordId] = record; + } + } + + callback(inserts, removes, records); + }); +} + +subscribeToTagDiff("editor", (inserts, removes, records) => { + if(!client.showIDE) return; + client.ide.updateActions(inserts, removes, records); +}); + +subscribeToTagDiff("view", (inserts, removes, records) => { + if(!client.showIDE) return; + client.ide.updateViews(inserts, removes, records) +}); + +//--------------------------------------------------------- +// Communication helpers +//--------------------------------------------------------- + +function recordToEAVs(record) { + if(!record) return; + let eavs:EAV[] = []; + if(record.id && record.id.constructor === Array) throw new Error("Unable to apply multiple ids to the same record: " + JSON.stringify(record)); + if(!record.id) record.id = uuid(); + record.id = "" + record.id + ""; + let e = record.id; + + for(let a in record) { + if(record[a] === undefined) continue; + if(a === "id") continue; + if(record[a].constructor === Array) { + for(let v of record[a]) { + if(typeof v === "object") { + eavs.push.apply(eavs, recordToEAVs(v)); + eavs.push([e, a, v.id]); + } else if(v !== undefined) { + eavs.push([e, a, v]); + } + } + } else { + let v = record[a]; + if(typeof v === "object") { + eavs.push.apply(eavs, recordToEAVs(v)); + eavs.push([e, a, v.id]); + } else if(v !== undefined) { + eavs.push([e, a, v]); + } + } + } + return eavs; +} + +//--------------------------------------------------------- +// Initialize an IDE +//--------------------------------------------------------- +export let client = new EveClient(); + +function initIDE(client:EveClient) { + let ide = client.ide; + ide.onChange = (ide:IDE) => { + let generation = ide.generation; + let md = ide.editor.toMarkdown(); + console.groupCollapsed(`SENT ${generation}`); + console.info(md); + console.groupEnd(); + client.send({scope: "root", type: "parse", generation, code: md}); + } + ide.onEval = (ide:IDE, persist) => { + client.send({type: "eval", persist}); + } + ide.onLoadFile = (ide, documentId, code) => { + client.send({type: "close"}); + client.send({scope: "root", type: "parse", code}) + client.send({type: "eval", persist: false}); + let url = `${location.pathname}#${documentId}`; + history.pushState({}, "", url + location.search); + analyticsEvent("load-document", documentId); + } + + ide.onSaveDocument = (ide, documentId, code) => { + client.sendControl(JSON.stringify({type: "save", path: documentId, code})); + } + + ide.onTokenInfo = (ide, tokenId) => { + client.send({type: "tokenInfo", tokenId}); + } + + let cache = window["_workspaceCache"]; + for(let workspace in cache || {}) { + ide.loadWorkspace(workspace, cache[workspace]); + } +} + +function changeDocument() { + if(!client.showIDE) return; + let ide = client.ide; + // @FIXME: This is not right in the non-internal case. + let docId = "/examples/quickstart.eve"; + let path = location.hash && location.hash.split('?')[0].split("#/")[1]; + if(path && path.length > 2) { + if(path[path.length - 1] === "/") path = path.slice(0, -1); + docId = "/" + path; + } + if(!docId) return; + if(docId === ide.documentId) return; + try { + ide.loadFile(docId); + } catch(err) { + ide.injectNotice("info", "Unable to load unknown file: " + docId); + } + ide.render(); +} + +//--------------------------------------------------------- +// Handlers +//--------------------------------------------------------- + +function onHashChange(event) { + if(client.ide && client.ide.loaded) changeDocument(); + let hash = window.location.hash.split("#/")[2]; + let queryParam = window.location.hash.split('?')[1]; + + if(hash || queryParam) { + let segments = (hash||'').split("/").map(function(seg, ix) { + return {id: uuid(), index: ix + 1, value: seg}; + }), queries = (queryParam||'').split('&').map(function (kv) { + let [k, v] = kv.split('=',2); + return {id: uuid(), key: k, value: v}; + }); + + client.sendEvent([ + {tag: "url-change", "hash-segment": segments, "query-param": queries} + ]); + } +} + +window.addEventListener("hashchange", onHashChange); + +window.document.body.addEventListener("dragover", (e) => { + e.preventDefault(); +}) + +window.document.body.addEventListener("drop", (e) => { + if(e.dataTransfer.files.length) { + let reader = new FileReader(); + reader.onload = function (event) { + socket.send(`{"type": "load", "info": ${reader.result}}`); + }; + reader.readAsText(e.dataTransfer.files[0]); + } + e.preventDefault(); + e.stopPropagation(); +}); diff --git a/src/codemirror.css b/src/codemirror.css new file mode 100644 index 000000000..18b0bf70d --- /dev/null +++ b/src/codemirror.css @@ -0,0 +1,347 @@ +/* BASICS */ + +.CodeMirror { + /* Set height, width, borders, and global font properties here */ + font-family: monospace; + height: 300px; + color: black; +} + +/* PADDING */ + +.CodeMirror-lines { + padding: 4px 0; /* Vertical padding around content */ +} +.CodeMirror pre { + padding: 0 4px; /* Horizontal padding of content */ +} + +.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + background-color: white; /* The little square between H and V scrollbars */ +} + +/* GUTTER */ + +.CodeMirror-gutters { + border-right: 1px solid #ddd; + background-color: #f7f7f7; + white-space: nowrap; +} +.CodeMirror-linenumbers {} +.CodeMirror-linenumber { + padding: 0 3px 0 5px; + min-width: 20px; + text-align: right; + color: #999; + white-space: nowrap; +} + +.CodeMirror-guttermarker { color: black; } +.CodeMirror-guttermarker-subtle { color: #999; } + +/* CURSOR */ + +.CodeMirror-cursor { + border-left: 1px solid black; + border-right: none; + width: 0; +} +/* Shown when moving in bi-directional text */ +.CodeMirror div.CodeMirror-secondarycursor { + border-left: 1px solid silver; +} +.cm-fat-cursor .CodeMirror-cursor { + width: auto; + border: 0 !important; + background: #7e7; +} +.cm-fat-cursor div.CodeMirror-cursors { + z-index: 1; +} + +.cm-animate-fat-cursor { + width: auto; + border: 0; + -webkit-animation: blink 1.06s steps(1) infinite; + -moz-animation: blink 1.06s steps(1) infinite; + animation: blink 1.06s steps(1) infinite; + background-color: #7e7; +} +@-moz-keyframes blink { + 0% {} + 50% { background-color: transparent; } + 100% {} +} +@-webkit-keyframes blink { + 0% {} + 50% { background-color: transparent; } + 100% {} +} +@keyframes blink { + 0% {} + 50% { background-color: transparent; } + 100% {} +} + +/* Can style cursor different in overwrite (non-insert) mode */ +.CodeMirror-overwrite .CodeMirror-cursor {} + +.cm-tab { display: inline-block; text-decoration: inherit; } + +.CodeMirror-rulers { + position: absolute; + left: 0; right: 0; top: -50px; bottom: -20px; + overflow: hidden; +} +.CodeMirror-ruler { + border-left: 1px solid #ccc; + top: 0; bottom: 0; + position: absolute; +} + +/* DEFAULT THEME */ + +.cm-s-default .cm-header {color: blue;} +.cm-s-default .cm-quote {color: #090;} +.cm-negative {color: #d44;} +.cm-positive {color: #292;} +.cm-header, .cm-strong {font-weight: bold;} +.cm-em {font-style: italic;} +.cm-link {text-decoration: underline;} +.cm-strikethrough {text-decoration: line-through;} + +.cm-s-default .cm-keyword {color: #708;} +.cm-s-default .cm-atom {color: #219;} +.cm-s-default .cm-number {color: #164;} +.cm-s-default .cm-def {color: #00f;} +.cm-s-default .cm-variable, +.cm-s-default .cm-punctuation, +.cm-s-default .cm-property, +.cm-s-default .cm-operator {} +.cm-s-default .cm-variable-2 {color: #05a;} +.cm-s-default .cm-variable-3 {color: #085;} +.cm-s-default .cm-comment {color: #a50;} +.cm-s-default .cm-string {color: #a11;} +.cm-s-default .cm-string-2 {color: #f50;} +.cm-s-default .cm-meta {color: #555;} +.cm-s-default .cm-qualifier {color: #555;} +.cm-s-default .cm-builtin {color: #30a;} +.cm-s-default .cm-bracket {color: #997;} +.cm-s-default .cm-tag {color: #170;} +.cm-s-default .cm-attribute {color: #00c;} +.cm-s-default .cm-hr {color: #999;} +.cm-s-default .cm-link {color: #00c;} + +.cm-s-default .cm-error {color: #f00;} +.cm-invalidchar {color: #f00;} + +.CodeMirror-composing { border-bottom: 2px solid; } + +/* Default styles for common addons */ + +div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} +div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} +.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } +.CodeMirror-activeline-background {background: #e8f2ff;} + +/* STOP */ + +/* The rest of this file contains styles related to the mechanics of + the editor. You probably shouldn't touch them. */ + +.CodeMirror { + position: relative; + overflow: hidden; + background: white; +} + +.CodeMirror-scroll { + overflow: scroll !important; /* Things will break if this is overridden */ + /* 30px is the magic margin used to hide the element's real scrollbars */ + /* See overflow: hidden in .CodeMirror */ + margin-bottom: -30px; margin-right: -30px; + padding-bottom: 30px; + height: 100%; + outline: none; /* Prevent dragging from highlighting the element */ + position: relative; +} +.CodeMirror-sizer { + position: relative; + border-right: 30px solid transparent; +} + +/* The fake, visible scrollbars. Used to force redraw during scrolling + before actual scrolling happens, thus preventing shaking and + flickering artifacts. */ +.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + position: absolute; + z-index: 6; + display: none; +} +.CodeMirror-vscrollbar { + right: 0; top: 0; + overflow-x: hidden; + overflow-y: scroll; +} +.CodeMirror-hscrollbar { + bottom: 0; left: 0; + overflow-y: hidden; + overflow-x: scroll; +} +.CodeMirror-scrollbar-filler { + right: 0; bottom: 0; +} +.CodeMirror-gutter-filler { + left: 0; bottom: 0; +} + +.CodeMirror-gutters { + position: absolute; left: 0; top: 0; + min-height: 100%; + z-index: 3; +} +.CodeMirror-gutter { + white-space: normal; + height: 100%; + display: inline-block; + vertical-align: top; + margin-bottom: -30px; + /* Hack to make IE7 behave */ + *zoom:1; + *display:inline; +} +.CodeMirror-gutter-wrapper { + position: absolute; + z-index: 4; + background: none !important; + border: none !important; +} +.CodeMirror-gutter-background { + position: absolute; + top: 0; bottom: 0; + z-index: 4; +} +.CodeMirror-gutter-elt { + position: absolute; + cursor: default; + z-index: 4; +} +.CodeMirror-gutter-wrapper { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.CodeMirror-lines { + cursor: text; + min-height: 1px; /* prevents collapsing before first draw */ +} +.CodeMirror pre { + /* Reset some styles that the rest of the page might have set */ + -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; + border-width: 0; + background: transparent; + font-family: inherit; + font-size: inherit; + margin: 0; + white-space: pre; + word-wrap: normal; + line-height: inherit; + color: inherit; + z-index: 2; + position: relative; + overflow: visible; + -webkit-tap-highlight-color: transparent; + -webkit-font-variant-ligatures: none; + font-variant-ligatures: none; +} +.CodeMirror-wrap pre { + word-wrap: break-word; + white-space: pre-wrap; + word-break: normal; +} + +.CodeMirror-linebackground { + position: absolute; + left: 0; right: 0; top: 0; bottom: 0; + z-index: 0; +} + +.CodeMirror-linewidget { + position: relative; + z-index: 2; + overflow: auto; +} + +.CodeMirror-widget {} + +.CodeMirror-code { + outline: none; +} + +/* Force content-box sizing for the elements where we expect it */ +.CodeMirror-scroll, +.CodeMirror-sizer, +.CodeMirror-gutter, +.CodeMirror-gutters, +.CodeMirror-linenumber { + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +.CodeMirror-measure { + position: absolute; + width: 100%; + height: 0; + overflow: hidden; + visibility: hidden; +} + +.CodeMirror-cursor { + position: absolute; + pointer-events: none; +} +.CodeMirror-measure pre { position: static; } + +div.CodeMirror-cursors { + visibility: hidden; + position: relative; + z-index: 3; +} +div.CodeMirror-dragcursors { + visibility: visible; +} + +.CodeMirror-focused div.CodeMirror-cursors { + visibility: visible; +} + +.CodeMirror-selected { background: #d9d9d9; } +.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } +.CodeMirror-crosshair { cursor: crosshair; } +.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } +.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } + +.cm-searching { + background: #ffa; + background: rgba(255, 255, 0, .4); +} + +/* IE7 hack to prevent it from returning funny offsetTops on the spans */ +.CodeMirror span { *vertical-align: text-bottom; } + +/* Used to force a border model for a node */ +.cm-force-border { padding-right: .1px; } + +@media print { + /* Hide the cursor when printing */ + .CodeMirror div.CodeMirror-cursors { + visibility: hidden; + } +} + +/* See issue #2901 */ +.cm-tab-wrap-hack:after { content: ''; } + +/* Help users use markselection to safely style text background */ +span.CodeMirror-selectedtext { background: none; } diff --git a/src/codemirror.js b/src/codemirror.js new file mode 100644 index 000000000..de43229f7 --- /dev/null +++ b/src/codemirror.js @@ -0,0 +1,14455 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +// This is CodeMirror (http://codemirror.net), a code editor +// implemented in JavaScript on top of the browser's DOM. +// +// You can find some technical background for some of the code below +// at http://marijnhaverbeke.nl/blog/#cm-internals . + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + module.exports = mod(); + else if (typeof define == "function" && define.amd) // AMD + return define([], mod); + else // Plain browser env + (this || window).CodeMirror = mod(); +})(function() { + "use strict"; + + // BROWSER SNIFFING + + // Kludges for bugs and behavior differences that can't be feature + // detected are enabled based on userAgent etc sniffing. + var userAgent = navigator.userAgent; + var platform = navigator.platform; + + var gecko = /gecko\/\d/i.test(userAgent); + var ie_upto10 = /MSIE \d/.test(userAgent); + var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(userAgent); + var ie = ie_upto10 || ie_11up; + var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : ie_11up[1]); + var webkit = /WebKit\//.test(userAgent); + var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(userAgent); + var chrome = /Chrome\//.test(userAgent); + var presto = /Opera\//.test(userAgent); + var safari = /Apple Computer/.test(navigator.vendor); + var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(userAgent); + var phantom = /PhantomJS/.test(userAgent); + + var ios = /AppleWebKit/.test(userAgent) && /Mobile\/\w+/.test(userAgent); + // This is woefully incomplete. Suggestions for alternative methods welcome. + var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(userAgent); + var mac = ios || /Mac/.test(platform); + var chromeOS = /\bCrOS\b/.test(userAgent); + var windows = /win/i.test(platform); + + var presto_version = presto && userAgent.match(/Version\/(\d*\.\d*)/); + if (presto_version) presto_version = Number(presto_version[1]); + if (presto_version && presto_version >= 15) { presto = false; webkit = true; } + // Some browsers use the wrong event properties to signal cmd/ctrl on OS X + var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11)); + var captureRightClick = gecko || (ie && ie_version >= 9); + + // Optimize some code when these features are not used. + var sawReadOnlySpans = false, sawCollapsedSpans = false; + + // EDITOR CONSTRUCTOR + + // A CodeMirror instance represents an editor. This is the object + // that user code is usually dealing with. + + function CodeMirror(place, options) { + if (!(this instanceof CodeMirror)) return new CodeMirror(place, options); + + this.options = options = options ? copyObj(options) : {}; + // Determine effective options based on given values and defaults. + copyObj(defaults, options, false); + setGuttersForLineNumbers(options); + + var doc = options.value; + if (typeof doc == "string") doc = new Doc(doc, options.mode, null, options.lineSeparator); + this.doc = doc; + + var input = new CodeMirror.inputStyles[options.inputStyle](this); + var display = this.display = new Display(place, doc, input); + display.wrapper.CodeMirror = this; + updateGutters(this); + themeChanged(this); + if (options.lineWrapping) + this.display.wrapper.className += " CodeMirror-wrap"; + if (options.autofocus && !mobile) display.input.focus(); + initScrollbars(this); + + this.state = { + keyMaps: [], // stores maps added by addKeyMap + overlays: [], // highlighting overlays, as added by addOverlay + modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info + overwrite: false, + delayingBlurEvent: false, + focused: false, + suppressEdits: false, // used to disable editing during key handlers when in readOnly mode + pasteIncoming: false, cutIncoming: false, // help recognize paste/cut edits in input.poll + selectingText: false, + draggingText: false, + highlight: new Delayed(), // stores highlight worker timeout + keySeq: null, // Unfinished key sequence + specialChars: null + }; + + var cm = this; + + // Override magic textarea content restore that IE sometimes does + // on our hidden textarea on reload + if (ie && ie_version < 11) setTimeout(function() { cm.display.input.reset(true); }, 20); + + registerEventHandlers(this); + ensureGlobalHandlers(); + + startOperation(this); + this.curOp.forceUpdate = true; + attachDoc(this, doc); + + if ((options.autofocus && !mobile) || cm.hasFocus()) + setTimeout(bind(onFocus, this), 20); + else + onBlur(this); + + for (var opt in optionHandlers) if (optionHandlers.hasOwnProperty(opt)) + optionHandlers[opt](this, options[opt], Init); + maybeUpdateLineNumberWidth(this); + if (options.finishInit) options.finishInit(this); + for (var i = 0; i < initHooks.length; ++i) initHooks[i](this); + endOperation(this); + // Suppress optimizelegibility in Webkit, since it breaks text + // measuring on line wrapping boundaries. + if (webkit && options.lineWrapping && + getComputedStyle(display.lineDiv).textRendering == "optimizelegibility") + display.lineDiv.style.textRendering = "auto"; + } + + // DISPLAY CONSTRUCTOR + + // The display handles the DOM integration, both for input reading + // and content drawing. It holds references to DOM nodes and + // display-related state. + + function Display(place, doc, input) { + var d = this; + this.input = input; + + // Covers bottom-right square when both scrollbars are present. + d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler"); + d.scrollbarFiller.setAttribute("cm-not-content", "true"); + // Covers bottom of gutter when coverGutterNextToScrollbar is on + // and h scrollbar is present. + d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler"); + d.gutterFiller.setAttribute("cm-not-content", "true"); + // Will contain the actual code, positioned to cover the viewport. + d.lineDiv = elt("div", null, "CodeMirror-code"); + // Elements are added to these to represent selection and cursors. + d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1"); + d.cursorDiv = elt("div", null, "CodeMirror-cursors"); + // A visibility: hidden element used to find the size of things. + d.measure = elt("div", null, "CodeMirror-measure"); + // When lines outside of the viewport are measured, they are drawn in this. + d.lineMeasure = elt("div", null, "CodeMirror-measure"); + // Wraps everything that needs to exist inside the vertically-padded coordinate system + d.lineSpace = elt("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv], + null, "position: relative; outline: none"); + // Moved around its parent to cover visible view. + d.mover = elt("div", [elt("div", [d.lineSpace], "CodeMirror-lines")], null, "position: relative"); + // Set to the height of the document, allowing scrolling. + d.sizer = elt("div", [d.mover], "CodeMirror-sizer"); + d.sizerWidth = null; + // Behavior of elts with overflow: auto and padding is + // inconsistent across browsers. This is used to ensure the + // scrollable area is big enough. + d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerGap + "px; width: 1px;"); + // Will contain the gutters, if any. + d.gutters = elt("div", null, "CodeMirror-gutters"); + d.lineGutter = null; + // Actual scrollable element. + d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll"); + d.scroller.setAttribute("tabIndex", "-1"); + // The element in which the editor lives. + d.wrapper = elt("div", [d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror"); + + // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported) + if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } + if (!webkit && !(gecko && mobile)) d.scroller.draggable = true; + + if (place) { + if (place.appendChild) place.appendChild(d.wrapper); + else place(d.wrapper); + } + + // Current rendered range (may be bigger than the view window). + d.viewFrom = d.viewTo = doc.first; + d.reportedViewFrom = d.reportedViewTo = doc.first; + // Information about the rendered lines. + d.view = []; + d.renderedView = null; + // Holds info about a single rendered line when it was rendered + // for measurement, while not in view. + d.externalMeasured = null; + // Empty space (in pixels) above the view + d.viewOffset = 0; + d.lastWrapHeight = d.lastWrapWidth = 0; + d.updateLineNumbers = null; + + d.nativeBarWidth = d.barHeight = d.barWidth = 0; + d.scrollbarsClipped = false; + + // Used to only resize the line number gutter when necessary (when + // the amount of lines crosses a boundary that makes its width change) + d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null; + // Set to true when a non-horizontal-scrolling line widget is + // added. As an optimization, line widget aligning is skipped when + // this is false. + d.alignWidgets = false; + + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; + + // Tracks the maximum line length so that the horizontal scrollbar + // can be kept static when scrolling. + d.maxLine = null; + d.maxLineLength = 0; + d.maxLineChanged = false; + + // Used for measuring wheel scrolling granularity + d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null; + + // True when shift is held down. + d.shift = false; + + // Used to track whether anything happened since the context menu + // was opened. + d.selForContextMenu = null; + + d.activeTouch = null; + + input.init(d); + } + + // STATE UPDATES + + // Used to get the editor into a consistent state again when options change. + + function loadMode(cm) { + cm.doc.mode = CodeMirror.getMode(cm.options, cm.doc.modeOption); + resetModeState(cm); + } + + function resetModeState(cm) { + cm.doc.iter(function(line) { + if (line.stateAfter) line.stateAfter = null; + if (line.styles) line.styles = null; + }); + cm.doc.frontier = cm.doc.first; + startWorker(cm, 100); + cm.state.modeGen++; + if (cm.curOp) regChange(cm); + } + + function wrappingChanged(cm) { + if (cm.options.lineWrapping) { + addClass(cm.display.wrapper, "CodeMirror-wrap"); + cm.display.sizer.style.minWidth = ""; + cm.display.sizerWidth = null; + } else { + rmClass(cm.display.wrapper, "CodeMirror-wrap"); + findMaxLine(cm); + } + estimateLineHeights(cm); + regChange(cm); + clearCaches(cm); + setTimeout(function(){updateScrollbars(cm);}, 100); + } + + // Returns a function that estimates the height of a line, to use as + // first approximation until the line becomes visible (and is thus + // properly measurable). + function estimateHeight(cm) { + var th = textHeight(cm.display), wrapping = cm.options.lineWrapping; + var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3); + return function(line) { + if (lineIsHidden(cm.doc, line)) return 0; + + var widgetsHeight = 0; + if (line.widgets) for (var i = 0; i < line.widgets.length; i++) { + if (line.widgets[i].height) widgetsHeight += line.widgets[i].height; + } + + if (wrapping) + return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th; + else + return widgetsHeight + th; + }; + } + + function estimateLineHeights(cm) { + var doc = cm.doc, est = estimateHeight(cm); + doc.iter(function(line) { + var estHeight = est(line); + if (estHeight != line.height) updateLineHeight(line, estHeight); + }); + } + + function themeChanged(cm) { + cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") + + cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-"); + clearCaches(cm); + } + + function guttersChanged(cm) { + updateGutters(cm); + regChange(cm); + setTimeout(function(){alignHorizontally(cm);}, 20); + } + + // Rebuild the gutter elements, ensure the margin to the left of the + // code matches their width. + function updateGutters(cm) { + var gutters = cm.display.gutters, specs = cm.options.gutters; + removeChildren(gutters); + for (var i = 0; i < specs.length; ++i) { + var gutterClass = specs[i]; + var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass)); + if (gutterClass == "CodeMirror-linenumbers") { + cm.display.lineGutter = gElt; + gElt.style.width = (cm.display.lineNumWidth || 1) + "px"; + } + } + gutters.style.display = i ? "" : "none"; + updateGutterSpace(cm); + } + + function updateGutterSpace(cm) { + var width = cm.display.gutters.offsetWidth; + cm.display.sizer.style.marginLeft = width + "px"; + } + + // Compute the character length of a line, taking into account + // collapsed ranges (see markText) that might hide parts, and join + // other lines onto it. + function lineLength(line) { + if (line.height == 0) return 0; + var len = line.text.length, merged, cur = line; + while (merged = collapsedSpanAtStart(cur)) { + var found = merged.find(0, true); + cur = found.from.line; + len += found.from.ch - found.to.ch; + } + cur = line; + while (merged = collapsedSpanAtEnd(cur)) { + var found = merged.find(0, true); + len -= cur.text.length - found.from.ch; + cur = found.to.line; + len += cur.text.length - found.to.ch; + } + return len; + } + + // Find the longest line in the document. + function findMaxLine(cm) { + var d = cm.display, doc = cm.doc; + d.maxLine = getLine(doc, doc.first); + d.maxLineLength = lineLength(d.maxLine); + d.maxLineChanged = true; + doc.iter(function(line) { + var len = lineLength(line); + if (len > d.maxLineLength) { + d.maxLineLength = len; + d.maxLine = line; + } + }); + } + + // Make sure the gutters options contains the element + // "CodeMirror-linenumbers" when the lineNumbers option is true. + function setGuttersForLineNumbers(options) { + var found = indexOf(options.gutters, "CodeMirror-linenumbers"); + if (found == -1 && options.lineNumbers) { + options.gutters = options.gutters.concat(["CodeMirror-linenumbers"]); + } else if (found > -1 && !options.lineNumbers) { + options.gutters = options.gutters.slice(0); + options.gutters.splice(found, 1); + } + } + + // SCROLLBARS + + // Prepare DOM reads needed to update the scrollbars. Done in one + // shot to minimize update/measure roundtrips. + function measureForScrollbars(cm) { + var d = cm.display, gutterW = d.gutters.offsetWidth; + var docH = Math.round(cm.doc.height + paddingVert(cm.display)); + return { + clientHeight: d.scroller.clientHeight, + viewHeight: d.wrapper.clientHeight, + scrollWidth: d.scroller.scrollWidth, clientWidth: d.scroller.clientWidth, + viewWidth: d.wrapper.clientWidth, + barLeft: cm.options.fixedGutter ? gutterW : 0, + docHeight: docH, + scrollHeight: docH + scrollGap(cm) + d.barHeight, + nativeBarWidth: d.nativeBarWidth, + gutterWidth: gutterW + }; + } + + function NativeScrollbars(place, scroll, cm) { + this.cm = cm; + var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar"); + var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar"); + place(vert); place(horiz); + + on(vert, "scroll", function() { + if (vert.clientHeight) scroll(vert.scrollTop, "vertical"); + }); + on(horiz, "scroll", function() { + if (horiz.clientWidth) scroll(horiz.scrollLeft, "horizontal"); + }); + + this.checkedZeroWidth = false; + // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). + if (ie && ie_version < 8) this.horiz.style.minHeight = this.vert.style.minWidth = "18px"; + } + + NativeScrollbars.prototype = copyObj({ + update: function(measure) { + var needsH = measure.scrollWidth > measure.clientWidth + 1; + var needsV = measure.scrollHeight > measure.clientHeight + 1; + var sWidth = measure.nativeBarWidth; + + if (needsV) { + this.vert.style.display = "block"; + this.vert.style.bottom = needsH ? sWidth + "px" : "0"; + var totalHeight = measure.viewHeight - (needsH ? sWidth : 0); + // A bug in IE8 can cause this value to be negative, so guard it. + this.vert.firstChild.style.height = + Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px"; + } else { + this.vert.style.display = ""; + this.vert.firstChild.style.height = "0"; + } + + if (needsH) { + this.horiz.style.display = "block"; + this.horiz.style.right = needsV ? sWidth + "px" : "0"; + this.horiz.style.left = measure.barLeft + "px"; + var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0); + this.horiz.firstChild.style.width = + (measure.scrollWidth - measure.clientWidth + totalWidth) + "px"; + } else { + this.horiz.style.display = ""; + this.horiz.firstChild.style.width = "0"; + } + + if (!this.checkedZeroWidth && measure.clientHeight > 0) { + if (sWidth == 0) this.zeroWidthHack(); + this.checkedZeroWidth = true; + } + + return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0}; + }, + setScrollLeft: function(pos) { + if (this.horiz.scrollLeft != pos) this.horiz.scrollLeft = pos; + if (this.disableHoriz) this.enableZeroWidthBar(this.horiz, this.disableHoriz); + }, + setScrollTop: function(pos) { + if (this.vert.scrollTop != pos) this.vert.scrollTop = pos; + if (this.disableVert) this.enableZeroWidthBar(this.vert, this.disableVert); + }, + zeroWidthHack: function() { + var w = mac && !mac_geMountainLion ? "12px" : "18px"; + this.horiz.style.height = this.vert.style.width = w; + this.horiz.style.pointerEvents = this.vert.style.pointerEvents = "none"; + this.disableHoriz = new Delayed; + this.disableVert = new Delayed; + }, + enableZeroWidthBar: function(bar, delay) { + bar.style.pointerEvents = "auto"; + function maybeDisable() { + // To find out whether the scrollbar is still visible, we + // check whether the element under the pixel in the bottom + // left corner of the scrollbar box is the scrollbar box + // itself (when the bar is still visible) or its filler child + // (when the bar is hidden). If it is still visible, we keep + // it enabled, if it's hidden, we disable pointer events. + var box = bar.getBoundingClientRect(); + var elt = document.elementFromPoint(box.left + 1, box.bottom - 1); + if (elt != bar) bar.style.pointerEvents = "none"; + else delay.set(1000, maybeDisable); + } + delay.set(1000, maybeDisable); + }, + clear: function() { + var parent = this.horiz.parentNode; + parent.removeChild(this.horiz); + parent.removeChild(this.vert); + } + }, NativeScrollbars.prototype); + + function NullScrollbars() {} + + NullScrollbars.prototype = copyObj({ + update: function() { return {bottom: 0, right: 0}; }, + setScrollLeft: function() {}, + setScrollTop: function() {}, + clear: function() {} + }, NullScrollbars.prototype); + + CodeMirror.scrollbarModel = {"native": NativeScrollbars, "null": NullScrollbars}; + + function initScrollbars(cm) { + if (cm.display.scrollbars) { + cm.display.scrollbars.clear(); + if (cm.display.scrollbars.addClass) + rmClass(cm.display.wrapper, cm.display.scrollbars.addClass); + } + + cm.display.scrollbars = new CodeMirror.scrollbarModel[cm.options.scrollbarStyle](function(node) { + cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller); + // Prevent clicks in the scrollbars from killing focus + on(node, "mousedown", function() { + if (cm.state.focused) setTimeout(function() { cm.display.input.focus(); }, 0); + }); + node.setAttribute("cm-not-content", "true"); + }, function(pos, axis) { + if (axis == "horizontal") setScrollLeft(cm, pos); + else setScrollTop(cm, pos); + }, cm); + if (cm.display.scrollbars.addClass) + addClass(cm.display.wrapper, cm.display.scrollbars.addClass); + } + + function updateScrollbars(cm, measure) { + if (!measure) measure = measureForScrollbars(cm); + var startWidth = cm.display.barWidth, startHeight = cm.display.barHeight; + updateScrollbarsInner(cm, measure); + for (var i = 0; i < 4 && startWidth != cm.display.barWidth || startHeight != cm.display.barHeight; i++) { + if (startWidth != cm.display.barWidth && cm.options.lineWrapping) + updateHeightsInViewport(cm); + updateScrollbarsInner(cm, measureForScrollbars(cm)); + startWidth = cm.display.barWidth; startHeight = cm.display.barHeight; + } + } + + // Re-synchronize the fake scrollbars with the actual size of the + // content. + function updateScrollbarsInner(cm, measure) { + var d = cm.display; + var sizes = d.scrollbars.update(measure); + + d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px"; + d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px"; + d.heightForcer.style.borderBottom = sizes.bottom + "px solid transparent" + + if (sizes.right && sizes.bottom) { + d.scrollbarFiller.style.display = "block"; + d.scrollbarFiller.style.height = sizes.bottom + "px"; + d.scrollbarFiller.style.width = sizes.right + "px"; + } else d.scrollbarFiller.style.display = ""; + if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) { + d.gutterFiller.style.display = "block"; + d.gutterFiller.style.height = sizes.bottom + "px"; + d.gutterFiller.style.width = measure.gutterWidth + "px"; + } else d.gutterFiller.style.display = ""; + } + + // Compute the lines that are visible in a given viewport (defaults + // the the current scroll position). viewport may contain top, + // height, and ensure (see op.scrollToPos) properties. + function visibleLines(display, doc, viewport) { + var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop; + top = Math.floor(top - paddingTop(display)); + var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight; + + var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom); + // Ensure is a {from: {line, ch}, to: {line, ch}} object, and + // forces those lines into the viewport (if possible). + if (viewport && viewport.ensure) { + var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line; + if (ensureFrom < from) { + from = ensureFrom; + to = lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight); + } else if (Math.min(ensureTo, doc.lastLine()) >= to) { + from = lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight); + to = ensureTo; + } + } + return {from: from, to: Math.max(to, from + 1)}; + } + + // LINE NUMBERS + + // Re-align line numbers and gutter marks to compensate for + // horizontal scrolling. + function alignHorizontally(cm) { + var display = cm.display, view = display.view; + if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) return; + var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft; + var gutterW = display.gutters.offsetWidth, left = comp + "px"; + for (var i = 0; i < view.length; i++) if (!view[i].hidden) { + if (cm.options.fixedGutter && view[i].gutter) + view[i].gutter.style.left = left; + var align = view[i].alignable; + if (align) for (var j = 0; j < align.length; j++) + align[j].style.left = left; + } + if (cm.options.fixedGutter) + display.gutters.style.left = (comp + gutterW) + "px"; + } + + // Used to ensure that the line number gutter is still the right + // size for the current document size. Returns true when an update + // is needed. + function maybeUpdateLineNumberWidth(cm) { + if (!cm.options.lineNumbers) return false; + var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display; + if (last.length != display.lineNumChars) { + var test = display.measure.appendChild(elt("div", [elt("div", last)], + "CodeMirror-linenumber CodeMirror-gutter-elt")); + var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW; + display.lineGutter.style.width = ""; + display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1; + display.lineNumWidth = display.lineNumInnerWidth + padding; + display.lineNumChars = display.lineNumInnerWidth ? last.length : -1; + display.lineGutter.style.width = display.lineNumWidth + "px"; + updateGutterSpace(cm); + return true; + } + return false; + } + + function lineNumberFor(options, i) { + return String(options.lineNumberFormatter(i + options.firstLineNumber)); + } + + // Computes display.scroller.scrollLeft + display.gutters.offsetWidth, + // but using getBoundingClientRect to get a sub-pixel-accurate + // result. + function compensateForHScroll(display) { + return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left; + } + + // DISPLAY DRAWING + + function DisplayUpdate(cm, viewport, force) { + var display = cm.display; + + this.viewport = viewport; + // Store some values that we'll need later (but don't want to force a relayout for) + this.visible = visibleLines(display, cm.doc, viewport); + this.editorIsHidden = !display.wrapper.offsetWidth; + this.wrapperHeight = display.wrapper.clientHeight; + this.wrapperWidth = display.wrapper.clientWidth; + this.oldDisplayWidth = displayWidth(cm); + this.force = force; + this.dims = getDimensions(cm); + this.events = []; + } + + DisplayUpdate.prototype.signal = function(emitter, type) { + if (hasHandler(emitter, type)) + this.events.push(arguments); + }; + DisplayUpdate.prototype.finish = function() { + for (var i = 0; i < this.events.length; i++) + signal.apply(null, this.events[i]); + }; + + function maybeClipScrollbars(cm) { + var display = cm.display; + if (!display.scrollbarsClipped && display.scroller.offsetWidth) { + display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth; + display.heightForcer.style.height = scrollGap(cm) + "px"; + display.sizer.style.marginBottom = -display.nativeBarWidth + "px"; + display.sizer.style.borderRightWidth = scrollGap(cm) + "px"; + display.scrollbarsClipped = true; + } + } + + // Does the actual updating of the line display. Bails out + // (returning false) when there is nothing to be done and forced is + // false. + function updateDisplayIfNeeded(cm, update) { + var display = cm.display, doc = cm.doc; + + if (update.editorIsHidden) { + resetView(cm); + return false; + } + + // Bail out if the visible area is already rendered and nothing changed. + if (!update.force && + update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) && + display.renderedView == display.view && countDirtyView(cm) == 0) + return false; + + if (maybeUpdateLineNumberWidth(cm)) { + resetView(cm); + update.dims = getDimensions(cm); + } + + // Compute a suitable new viewport (from & to) + var end = doc.first + doc.size; + var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first); + var to = Math.min(end, update.visible.to + cm.options.viewportMargin); + if (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom); + if (display.viewTo > to && display.viewTo - to < 20) to = Math.min(end, display.viewTo); + if (sawCollapsedSpans) { + from = visualLineNo(cm.doc, from); + to = visualLineEndNo(cm.doc, to); + } + + var different = from != display.viewFrom || to != display.viewTo || + display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth; + adjustView(cm, from, to); + + display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom)); + // Position the mover div to align with the current scroll position + cm.display.mover.style.top = display.viewOffset + "px"; + + var toUpdate = countDirtyView(cm); + if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view && + (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo)) + return false; + + // For big changes, we hide the enclosing element during the + // update, since that speeds up the operations on most browsers. + var focused = activeElt(); + if (toUpdate > 4) display.lineDiv.style.display = "none"; + patchDisplay(cm, display.updateLineNumbers, update.dims); + if (toUpdate > 4) display.lineDiv.style.display = ""; + display.renderedView = display.view; + // There might have been a widget with a focused element that got + // hidden or updated, if so re-focus it. + if (focused && activeElt() != focused && focused.offsetHeight) focused.focus(); + + // Prevent selection and cursors from interfering with the scroll + // width and height. + removeChildren(display.cursorDiv); + removeChildren(display.selectionDiv); + display.gutters.style.height = display.sizer.style.minHeight = 0; + + if (different) { + display.lastWrapHeight = update.wrapperHeight; + display.lastWrapWidth = update.wrapperWidth; + startWorker(cm, 400); + } + + display.updateLineNumbers = null; + + return true; + } + + function postUpdateDisplay(cm, update) { + var viewport = update.viewport; + + for (var first = true;; first = false) { + if (!first || !cm.options.lineWrapping || update.oldDisplayWidth == displayWidth(cm)) { + // Clip forced viewport to actual scrollable area. + if (viewport && viewport.top != null) + viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)}; + // Updated line heights might result in the drawn area not + // actually covering the viewport. Keep looping until it does. + update.visible = visibleLines(cm.display, cm.doc, viewport); + if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo) + break; + } + if (!updateDisplayIfNeeded(cm, update)) break; + updateHeightsInViewport(cm); + var barMeasure = measureForScrollbars(cm); + updateSelection(cm); + updateScrollbars(cm, barMeasure); + setDocumentHeight(cm, barMeasure); + } + + update.signal(cm, "update", cm); + if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) { + update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo); + cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo; + } + } + + function updateDisplaySimple(cm, viewport) { + var update = new DisplayUpdate(cm, viewport); + if (updateDisplayIfNeeded(cm, update)) { + updateHeightsInViewport(cm); + postUpdateDisplay(cm, update); + var barMeasure = measureForScrollbars(cm); + updateSelection(cm); + updateScrollbars(cm, barMeasure); + setDocumentHeight(cm, barMeasure); + update.finish(); + } + } + + function setDocumentHeight(cm, measure) { + cm.display.sizer.style.minHeight = measure.docHeight + "px"; + cm.display.heightForcer.style.top = measure.docHeight + "px"; + cm.display.gutters.style.height = (measure.docHeight + cm.display.barHeight + scrollGap(cm)) + "px"; + } + + // Read the actual heights of the rendered lines, and update their + // stored heights to match. + function updateHeightsInViewport(cm) { + var display = cm.display; + var prevBottom = display.lineDiv.offsetTop; + for (var i = 0; i < display.view.length; i++) { + var cur = display.view[i], height; + if (cur.hidden) continue; + if (ie && ie_version < 8) { + var bot = cur.node.offsetTop + cur.node.offsetHeight; + height = bot - prevBottom; + prevBottom = bot; + } else { + var box = cur.node.getBoundingClientRect(); + height = box.bottom - box.top; + } + var diff = cur.line.height - height; + if (height < 2) height = textHeight(display); + if (diff > .001 || diff < -.001) { + updateLineHeight(cur.line, height); + updateWidgetHeight(cur.line); + if (cur.rest) for (var j = 0; j < cur.rest.length; j++) + updateWidgetHeight(cur.rest[j]); + } + } + } + + // Read and store the height of line widgets associated with the + // given line. + function updateWidgetHeight(line) { + if (line.widgets) for (var i = 0; i < line.widgets.length; ++i) + line.widgets[i].height = line.widgets[i].node.parentNode.offsetHeight; + } + + // Do a bulk-read of the DOM positions and sizes needed to draw the + // view, so that we don't interleave reading and writing to the DOM. + function getDimensions(cm) { + var d = cm.display, left = {}, width = {}; + var gutterLeft = d.gutters.clientLeft; + for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) { + left[cm.options.gutters[i]] = n.offsetLeft + n.clientLeft + gutterLeft; + width[cm.options.gutters[i]] = n.clientWidth; + } + return {fixedPos: compensateForHScroll(d), + gutterTotalWidth: d.gutters.offsetWidth, + gutterLeft: left, + gutterWidth: width, + wrapperWidth: d.wrapper.clientWidth}; + } + + // Sync the actual display DOM structure with display.view, removing + // nodes for lines that are no longer in view, and creating the ones + // that are not there yet, and updating the ones that are out of + // date. + function patchDisplay(cm, updateNumbersFrom, dims) { + var display = cm.display, lineNumbers = cm.options.lineNumbers; + var container = display.lineDiv, cur = container.firstChild; + + function rm(node) { + var next = node.nextSibling; + // Works around a throw-scroll bug in OS X Webkit + if (webkit && mac && cm.display.currentWheelTarget == node) + node.style.display = "none"; + else + node.parentNode.removeChild(node); + return next; + } + + var view = display.view, lineN = display.viewFrom; + // Loop over the elements in the view, syncing cur (the DOM nodes + // in display.lineDiv) with the view as we go. + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (lineView.hidden) { + } else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet + var node = buildLineElement(cm, lineView, lineN, dims); + container.insertBefore(node, cur); + } else { // Already drawn + while (cur != lineView.node) cur = rm(cur); + var updateNumber = lineNumbers && updateNumbersFrom != null && + updateNumbersFrom <= lineN && lineView.lineNumber; + if (lineView.changes) { + if (indexOf(lineView.changes, "gutter") > -1) updateNumber = false; + updateLineForChanges(cm, lineView, lineN, dims); + } + if (updateNumber) { + removeChildren(lineView.lineNumber); + lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN))); + } + cur = lineView.node.nextSibling; + } + lineN += lineView.size; + } + while (cur) cur = rm(cur); + } + + // When an aspect of a line changes, a string is added to + // lineView.changes. This updates the relevant part of the line's + // DOM structure. + function updateLineForChanges(cm, lineView, lineN, dims) { + for (var j = 0; j < lineView.changes.length; j++) { + var type = lineView.changes[j]; + if (type == "text") updateLineText(cm, lineView); + else if (type == "gutter") updateLineGutter(cm, lineView, lineN, dims); + else if (type == "class") updateLineClasses(lineView); + else if (type == "widget") updateLineWidgets(cm, lineView, dims); + } + lineView.changes = null; + } + + // Lines with gutter elements, widgets or a background class need to + // be wrapped, and have the extra elements added to the wrapper div + function ensureLineWrapped(lineView) { + if (lineView.node == lineView.text) { + lineView.node = elt("div", null, null, "position: relative"); + if (lineView.text.parentNode) + lineView.text.parentNode.replaceChild(lineView.node, lineView.text); + lineView.node.appendChild(lineView.text); + if (ie && ie_version < 8) lineView.node.style.zIndex = 2; + } + return lineView.node; + } + + function updateLineBackground(lineView) { + var cls = lineView.bgClass ? lineView.bgClass + " " + (lineView.line.bgClass || "") : lineView.line.bgClass; + if (cls) cls += " CodeMirror-linebackground"; + if (lineView.background) { + if (cls) lineView.background.className = cls; + else { lineView.background.parentNode.removeChild(lineView.background); lineView.background = null; } + } else if (cls) { + var wrap = ensureLineWrapped(lineView); + lineView.background = wrap.insertBefore(elt("div", null, cls), wrap.firstChild); + } + } + + // Wrapper around buildLineContent which will reuse the structure + // in display.externalMeasured when possible. + function getLineContent(cm, lineView) { + var ext = cm.display.externalMeasured; + if (ext && ext.line == lineView.line) { + cm.display.externalMeasured = null; + lineView.measure = ext.measure; + return ext.built; + } + return buildLineContent(cm, lineView); + } + + // Redraw the line's text. Interacts with the background and text + // classes because the mode may output tokens that influence these + // classes. + function updateLineText(cm, lineView) { + var cls = lineView.text.className; + var built = getLineContent(cm, lineView); + if (lineView.text == lineView.node) lineView.node = built.pre; + lineView.text.parentNode.replaceChild(built.pre, lineView.text); + lineView.text = built.pre; + if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) { + lineView.bgClass = built.bgClass; + lineView.textClass = built.textClass; + updateLineClasses(lineView); + } else if (cls) { + lineView.text.className = cls; + } + } + + function updateLineClasses(lineView) { + updateLineBackground(lineView); + if (lineView.line.wrapClass) + ensureLineWrapped(lineView).className = lineView.line.wrapClass; + else if (lineView.node != lineView.text) + lineView.node.className = ""; + var textClass = lineView.textClass ? lineView.textClass + " " + (lineView.line.textClass || "") : lineView.line.textClass; + lineView.text.className = textClass || ""; + } + + function updateLineGutter(cm, lineView, lineN, dims) { + if (lineView.gutter) { + lineView.node.removeChild(lineView.gutter); + lineView.gutter = null; + } + if (lineView.gutterBackground) { + lineView.node.removeChild(lineView.gutterBackground); + lineView.gutterBackground = null; + } + if (lineView.line.gutterClass) { + var wrap = ensureLineWrapped(lineView); + lineView.gutterBackground = elt("div", null, "CodeMirror-gutter-background " + lineView.line.gutterClass, + "left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + + "px; width: " + dims.gutterTotalWidth + "px"); + wrap.insertBefore(lineView.gutterBackground, lineView.text); + } + var markers = lineView.line.gutterMarkers; + if (cm.options.lineNumbers || markers) { + var wrap = ensureLineWrapped(lineView); + var gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", "left: " + + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px"); + cm.display.input.setUneditable(gutterWrap); + wrap.insertBefore(gutterWrap, lineView.text); + if (lineView.line.gutterClass) + gutterWrap.className += " " + lineView.line.gutterClass; + if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"])) + lineView.lineNumber = gutterWrap.appendChild( + elt("div", lineNumberFor(cm.options, lineN), + "CodeMirror-linenumber CodeMirror-gutter-elt", + "left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: " + + cm.display.lineNumInnerWidth + "px")); + if (markers) for (var k = 0; k < cm.options.gutters.length; ++k) { + var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id]; + if (found) + gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", "left: " + + dims.gutterLeft[id] + "px; width: " + dims.gutterWidth[id] + "px")); + } + } + } + + function updateLineWidgets(cm, lineView, dims) { + if (lineView.alignable) lineView.alignable = null; + for (var node = lineView.node.firstChild, next; node; node = next) { + var next = node.nextSibling; + if (node.className == "CodeMirror-linewidget") + lineView.node.removeChild(node); + } + insertLineWidgets(cm, lineView, dims); + } + + // Build a line's DOM representation from scratch + function buildLineElement(cm, lineView, lineN, dims) { + var built = getLineContent(cm, lineView); + lineView.text = lineView.node = built.pre; + if (built.bgClass) lineView.bgClass = built.bgClass; + if (built.textClass) lineView.textClass = built.textClass; + + updateLineClasses(lineView); + updateLineGutter(cm, lineView, lineN, dims); + insertLineWidgets(cm, lineView, dims); + return lineView.node; + } + + // A lineView may contain multiple logical lines (when merged by + // collapsed spans). The widgets for all of them need to be drawn. + function insertLineWidgets(cm, lineView, dims) { + insertLineWidgetsFor(cm, lineView.line, lineView, dims, true); + if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++) + insertLineWidgetsFor(cm, lineView.rest[i], lineView, dims, false); + } + + function insertLineWidgetsFor(cm, line, lineView, dims, allowAbove) { + if (!line.widgets) return; + var wrap = ensureLineWrapped(lineView); + for (var i = 0, ws = line.widgets; i < ws.length; ++i) { + var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget"); + if (!widget.handleMouseEvents) node.setAttribute("cm-ignore-events", "true"); + positionLineWidget(widget, node, lineView, dims); + cm.display.input.setUneditable(node); + if (allowAbove && widget.above) + wrap.insertBefore(node, lineView.gutter || lineView.text); + else + wrap.appendChild(node); + signalLater(widget, "redraw"); + } + } + + function positionLineWidget(widget, node, lineView, dims) { + if (widget.noHScroll) { + (lineView.alignable || (lineView.alignable = [])).push(node); + var width = dims.wrapperWidth; + node.style.left = dims.fixedPos + "px"; + if (!widget.coverGutter) { + width -= dims.gutterTotalWidth; + node.style.paddingLeft = dims.gutterTotalWidth + "px"; + } + node.style.width = width + "px"; + } + if (widget.coverGutter) { + node.style.zIndex = 5; + node.style.position = "relative"; + if (!widget.noHScroll) node.style.marginLeft = -dims.gutterTotalWidth + "px"; + } + } + + // POSITION OBJECT + + // A Pos instance represents a position within the text. + var Pos = CodeMirror.Pos = function(line, ch) { + if (!(this instanceof Pos)) return new Pos(line, ch); + this.line = line; this.ch = ch; + }; + + // Compare two positions, return 0 if they are the same, a negative + // number when a is less, and a positive number otherwise. + var cmp = CodeMirror.cmpPos = function(a, b) { return a.line - b.line || a.ch - b.ch; }; + + function copyPos(x) {return Pos(x.line, x.ch);} + function maxPos(a, b) { return cmp(a, b) < 0 ? b : a; } + function minPos(a, b) { return cmp(a, b) < 0 ? a : b; } + + // INPUT HANDLING + + function ensureFocus(cm) { + if (!cm.state.focused) { cm.display.input.focus(); onFocus(cm); } + } + + // This will be set to a {lineWise: bool, text: [string]} object, so + // that, when pasting, we know what kind of selections the copied + // text was made out of. + var lastCopied = null; + + function applyTextInput(cm, inserted, deleted, sel, origin) { + var doc = cm.doc; + cm.display.shift = false; + if (!sel) sel = doc.sel; + + var paste = cm.state.pasteIncoming || origin == "paste"; + var textLines = doc.splitLines(inserted), multiPaste = null + // When pasing N lines into N selections, insert one line per selection + if (paste && sel.ranges.length > 1) { + if (lastCopied && lastCopied.text.join("\n") == inserted) { + if (sel.ranges.length % lastCopied.text.length == 0) { + multiPaste = []; + for (var i = 0; i < lastCopied.text.length; i++) + multiPaste.push(doc.splitLines(lastCopied.text[i])); + } + } else if (textLines.length == sel.ranges.length) { + multiPaste = map(textLines, function(l) { return [l]; }); + } + } + + // Normal behavior is to insert the new text into every selection + for (var i = sel.ranges.length - 1; i >= 0; i--) { + var range = sel.ranges[i]; + var from = range.from(), to = range.to(); + if (range.empty()) { + if (deleted && deleted > 0) // Handle deletion + from = Pos(from.line, from.ch - deleted); + else if (cm.state.overwrite && !paste) // Handle overwrite + to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length)); + else if (lastCopied && lastCopied.lineWise && lastCopied.text.join("\n") == inserted) + from = to = Pos(from.line, 0) + } + var updateInput = cm.curOp.updateInput; + var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i % multiPaste.length] : textLines, + origin: origin || (paste ? "paste" : cm.state.cutIncoming ? "cut" : "+input")}; + makeChange(cm.doc, changeEvent); + signalLater(cm, "inputRead", cm, changeEvent); + } + if (inserted && !paste) + triggerElectric(cm, inserted); + + ensureCursorVisible(cm); + cm.curOp.updateInput = updateInput; + cm.curOp.typing = true; + cm.state.pasteIncoming = cm.state.cutIncoming = false; + } + + function handlePaste(e, cm) { + var pasted = e.clipboardData && e.clipboardData.getData("text/plain"); + if (pasted) { + e.preventDefault(); + if (!cm.isReadOnly() && !cm.options.disableInput) + runInOp(cm, function() { applyTextInput(cm, pasted, 0, null, "paste"); }); + return true; + } + } + + function triggerElectric(cm, inserted) { + // When an 'electric' character is inserted, immediately trigger a reindent + if (!cm.options.electricChars || !cm.options.smartIndent) return; + var sel = cm.doc.sel; + + for (var i = sel.ranges.length - 1; i >= 0; i--) { + var range = sel.ranges[i]; + if (range.head.ch > 100 || (i && sel.ranges[i - 1].head.line == range.head.line)) continue; + var mode = cm.getModeAt(range.head); + var indented = false; + if (mode.electricChars) { + for (var j = 0; j < mode.electricChars.length; j++) + if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) { + indented = indentLine(cm, range.head.line, "smart"); + break; + } + } else if (mode.electricInput) { + if (mode.electricInput.test(getLine(cm.doc, range.head.line).text.slice(0, range.head.ch))) + indented = indentLine(cm, range.head.line, "smart"); + } + if (indented) signalLater(cm, "electricInput", cm, range.head.line); + } + } + + function copyableRanges(cm) { + var text = [], ranges = []; + for (var i = 0; i < cm.doc.sel.ranges.length; i++) { + var line = cm.doc.sel.ranges[i].head.line; + var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)}; + ranges.push(lineRange); + text.push(cm.getRange(lineRange.anchor, lineRange.head)); + } + return {text: text, ranges: ranges}; + } + + function disableBrowserMagic(field) { + field.setAttribute("autocorrect", "off"); + field.setAttribute("autocapitalize", "off"); + field.setAttribute("spellcheck", "false"); + } + + // TEXTAREA INPUT STYLE + + function TextareaInput(cm) { + this.cm = cm; + // See input.poll and input.reset + this.prevInput = ""; + + // Flag that indicates whether we expect input to appear real soon + // now (after some event like 'keypress' or 'input') and are + // polling intensively. + this.pollingFast = false; + // Self-resetting timeout for the poller + this.polling = new Delayed(); + // Tracks when input.reset has punted to just putting a short + // string into the textarea instead of the full selection. + this.inaccurateSelection = false; + // Used to work around IE issue with selection being forgotten when focus moves away from textarea + this.hasSelection = false; + this.composing = null; + }; + + function hiddenTextarea() { + var te = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none"); + var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); + // The textarea is kept positioned near the cursor to prevent the + // fact that it'll be scrolled into view on input from scrolling + // our fake cursor out of view. On webkit, when wrap=off, paste is + // very slow. So make the area wide instead. + if (webkit) te.style.width = "1000px"; + else te.setAttribute("wrap", "off"); + // If border: 0; -- iOS fails to open keyboard (issue #1287) + if (ios) te.style.border = "1px solid black"; + disableBrowserMagic(te); + return div; + } + + TextareaInput.prototype = copyObj({ + init: function(display) { + var input = this, cm = this.cm; + + // Wraps and hides input textarea + var div = this.wrapper = hiddenTextarea(); + // The semihidden textarea that is focused when the editor is + // focused, and receives input. + var te = this.textarea = div.firstChild; + display.wrapper.insertBefore(div, display.wrapper.firstChild); + + // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore) + if (ios) te.style.width = "0px"; + + on(te, "input", function() { + if (ie && ie_version >= 9 && input.hasSelection) input.hasSelection = null; + input.poll(); + }); + + on(te, "paste", function(e) { + if (signalDOMEvent(cm, e) || handlePaste(e, cm)) return + + cm.state.pasteIncoming = true; + input.fastPoll(); + }); + + function prepareCopyCut(e) { + if (signalDOMEvent(cm, e)) return + if (cm.somethingSelected()) { + lastCopied = {lineWise: false, text: cm.getSelections()}; + if (input.inaccurateSelection) { + input.prevInput = ""; + input.inaccurateSelection = false; + te.value = lastCopied.text.join("\n"); + selectInput(te); + } + } else if (!cm.options.lineWiseCopyCut) { + return; + } else { + var ranges = copyableRanges(cm); + lastCopied = {lineWise: true, text: ranges.text}; + if (e.type == "cut") { + cm.setSelections(ranges.ranges, null, sel_dontScroll); + } else { + input.prevInput = ""; + te.value = ranges.text.join("\n"); + selectInput(te); + } + } + if (e.type == "cut") cm.state.cutIncoming = true; + } + on(te, "cut", prepareCopyCut); + on(te, "copy", prepareCopyCut); + + on(display.scroller, "paste", function(e) { + if (eventInWidget(display, e) || signalDOMEvent(cm, e)) return; + cm.state.pasteIncoming = true; + input.focus(); + }); + + // Prevent normal selection in the editor (we handle our own) + on(display.lineSpace, "selectstart", function(e) { + if (!eventInWidget(display, e)) e_preventDefault(e); + }); + + on(te, "compositionstart", function() { + var start = cm.getCursor("from"); + if (input.composing) input.composing.range.clear() + input.composing = { + start: start, + range: cm.markText(start, cm.getCursor("to"), {className: "CodeMirror-composing"}) + }; + }); + on(te, "compositionend", function() { + if (input.composing) { + input.poll(); + input.composing.range.clear(); + input.composing = null; + } + }); + }, + + prepareSelection: function() { + // Redraw the selection and/or cursor + var cm = this.cm, display = cm.display, doc = cm.doc; + var result = prepareSelection(cm); + + // Move the hidden textarea near the cursor to prevent scrolling artifacts + if (cm.options.moveInputWithCursor) { + var headPos = cursorCoords(cm, doc.sel.primary().head, "div"); + var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect(); + result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10, + headPos.top + lineOff.top - wrapOff.top)); + result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10, + headPos.left + lineOff.left - wrapOff.left)); + } + + return result; + }, + + showSelection: function(drawn) { + var cm = this.cm, display = cm.display; + removeChildrenAndAdd(display.cursorDiv, drawn.cursors); + removeChildrenAndAdd(display.selectionDiv, drawn.selection); + if (drawn.teTop != null) { + this.wrapper.style.top = drawn.teTop + "px"; + this.wrapper.style.left = drawn.teLeft + "px"; + } + }, + + // Reset the input to correspond to the selection (or to be empty, + // when not typing and nothing is selected) + reset: function(typing) { + if (this.contextMenuPending) return; + var minimal, selected, cm = this.cm, doc = cm.doc; + if (cm.somethingSelected()) { + this.prevInput = ""; + var range = doc.sel.primary(); + minimal = hasCopyEvent && + (range.to().line - range.from().line > 100 || (selected = cm.getSelection()).length > 1000); + var content = minimal ? "-" : selected || cm.getSelection(); + this.textarea.value = content; + if (cm.state.focused) selectInput(this.textarea); + if (ie && ie_version >= 9) this.hasSelection = content; + } else if (!typing) { + this.prevInput = this.textarea.value = ""; + if (ie && ie_version >= 9) this.hasSelection = null; + } + this.inaccurateSelection = minimal; + }, + + getField: function() { return this.textarea; }, + + supportsTouch: function() { return false; }, + + focus: function() { + if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) { + try { this.textarea.focus(); } + catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM + } + }, + + blur: function() { this.textarea.blur(); }, + + resetPosition: function() { + this.wrapper.style.top = this.wrapper.style.left = 0; + }, + + receivedFocus: function() { this.slowPoll(); }, + + // Poll for input changes, using the normal rate of polling. This + // runs as long as the editor is focused. + slowPoll: function() { + var input = this; + if (input.pollingFast) return; + input.polling.set(this.cm.options.pollInterval, function() { + input.poll(); + if (input.cm.state.focused) input.slowPoll(); + }); + }, + + // When an event has just come in that is likely to add or change + // something in the input textarea, we poll faster, to ensure that + // the change appears on the screen quickly. + fastPoll: function() { + var missed = false, input = this; + input.pollingFast = true; + function p() { + var changed = input.poll(); + if (!changed && !missed) {missed = true; input.polling.set(60, p);} + else {input.pollingFast = false; input.slowPoll();} + } + input.polling.set(20, p); + }, + + // Read input from the textarea, and update the document to match. + // When something is selected, it is present in the textarea, and + // selected (unless it is huge, in which case a placeholder is + // used). When nothing is selected, the cursor sits after previously + // seen text (can be empty), which is stored in prevInput (we must + // not reset the textarea when typing, because that breaks IME). + poll: function() { + var cm = this.cm, input = this.textarea, prevInput = this.prevInput; + // Since this is called a *lot*, try to bail out as cheaply as + // possible when it is clear that nothing happened. hasSelection + // will be the case when there is a lot of text in the textarea, + // in which case reading its value would be expensive. + if (this.contextMenuPending || !cm.state.focused || + (hasSelection(input) && !prevInput && !this.composing) || + cm.isReadOnly() || cm.options.disableInput || cm.state.keySeq) + return false; + + var text = input.value; + // If nothing changed, bail. + if (text == prevInput && !cm.somethingSelected()) return false; + // Work around nonsensical selection resetting in IE9/10, and + // inexplicable appearance of private area unicode characters on + // some key combos in Mac (#2689). + if (ie && ie_version >= 9 && this.hasSelection === text || + mac && /[\uf700-\uf7ff]/.test(text)) { + cm.display.input.reset(); + return false; + } + + if (cm.doc.sel == cm.display.selForContextMenu) { + var first = text.charCodeAt(0); + if (first == 0x200b && !prevInput) prevInput = "\u200b"; + if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo"); } + } + // Find the part of the input that is actually new + var same = 0, l = Math.min(prevInput.length, text.length); + while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same; + + var self = this; + runInOp(cm, function() { + applyTextInput(cm, text.slice(same), prevInput.length - same, + null, self.composing ? "*compose" : null); + + // Don't leave long text in the textarea, since it makes further polling slow + if (text.length > 1000 || text.indexOf("\n") > -1) input.value = self.prevInput = ""; + else self.prevInput = text; + + if (self.composing) { + self.composing.range.clear(); + self.composing.range = cm.markText(self.composing.start, cm.getCursor("to"), + {className: "CodeMirror-composing"}); + } + }); + return true; + }, + + ensurePolled: function() { + if (this.pollingFast && this.poll()) this.pollingFast = false; + }, + + onKeyPress: function() { + if (ie && ie_version >= 9) this.hasSelection = null; + this.fastPoll(); + }, + + onContextMenu: function(e) { + var input = this, cm = input.cm, display = cm.display, te = input.textarea; + var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop; + if (!pos || presto) return; // Opera is difficult. + + // Reset the current text selection only if the click is done outside of the selection + // and 'resetSelectionOnContextMenu' option is true. + var reset = cm.options.resetSelectionOnContextMenu; + if (reset && cm.doc.sel.contains(pos) == -1) + operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll); + + var oldCSS = te.style.cssText, oldWrapperCSS = input.wrapper.style.cssText; + input.wrapper.style.cssText = "position: absolute" + var wrapperBox = input.wrapper.getBoundingClientRect() + te.style.cssText = "position: absolute; width: 30px; height: 30px; top: " + (e.clientY - wrapperBox.top - 5) + + "px; left: " + (e.clientX - wrapperBox.left - 5) + "px; z-index: 1000; background: " + + (ie ? "rgba(255, 255, 255, .05)" : "transparent") + + "; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; + if (webkit) var oldScrollY = window.scrollY; // Work around Chrome issue (#2712) + display.input.focus(); + if (webkit) window.scrollTo(null, oldScrollY); + display.input.reset(); + // Adds "Select all" to context menu in FF + if (!cm.somethingSelected()) te.value = input.prevInput = " "; + input.contextMenuPending = true; + display.selForContextMenu = cm.doc.sel; + clearTimeout(display.detectingSelectAll); + + // Select-all will be greyed out if there's nothing to select, so + // this adds a zero-width space so that we can later check whether + // it got selected. + function prepareSelectAllHack() { + if (te.selectionStart != null) { + var selected = cm.somethingSelected(); + var extval = "\u200b" + (selected ? te.value : ""); + te.value = "\u21da"; // Used to catch context-menu undo + te.value = extval; + input.prevInput = selected ? "" : "\u200b"; + te.selectionStart = 1; te.selectionEnd = extval.length; + // Re-set this, in case some other handler touched the + // selection in the meantime. + display.selForContextMenu = cm.doc.sel; + } + } + function rehide() { + input.contextMenuPending = false; + input.wrapper.style.cssText = oldWrapperCSS + te.style.cssText = oldCSS; + if (ie && ie_version < 9) display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos); + + // Try to detect the user choosing select-all + if (te.selectionStart != null) { + if (!ie || (ie && ie_version < 9)) prepareSelectAllHack(); + var i = 0, poll = function() { + if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 && + te.selectionEnd > 0 && input.prevInput == "\u200b") + operation(cm, commands.selectAll)(cm); + else if (i++ < 10) display.detectingSelectAll = setTimeout(poll, 500); + else display.input.reset(); + }; + display.detectingSelectAll = setTimeout(poll, 200); + } + } + + if (ie && ie_version >= 9) prepareSelectAllHack(); + if (captureRightClick) { + e_stop(e); + var mouseup = function() { + off(window, "mouseup", mouseup); + setTimeout(rehide, 20); + }; + on(window, "mouseup", mouseup); + } else { + setTimeout(rehide, 50); + } + }, + + readOnlyChanged: function(val) { + if (!val) this.reset(); + }, + + setUneditable: nothing, + + needsContentAttribute: false + }, TextareaInput.prototype); + + // CONTENTEDITABLE INPUT STYLE + + function ContentEditableInput(cm) { + this.cm = cm; + this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null; + this.polling = new Delayed(); + this.gracePeriod = false; + } + + ContentEditableInput.prototype = copyObj({ + init: function(display) { + var input = this, cm = input.cm; + var div = input.div = display.lineDiv; + disableBrowserMagic(div); + + on(div, "paste", function(e) { + if (!signalDOMEvent(cm, e)) handlePaste(e, cm); + }) + + on(div, "compositionstart", function(e) { + var data = e.data; + input.composing = {sel: cm.doc.sel, data: data, startData: data}; + if (!data) return; + var prim = cm.doc.sel.primary(); + var line = cm.getLine(prim.head.line); + var found = line.indexOf(data, Math.max(0, prim.head.ch - data.length)); + if (found > -1 && found <= prim.head.ch) + input.composing.sel = simpleSelection(Pos(prim.head.line, found), + Pos(prim.head.line, found + data.length)); + }); + on(div, "compositionupdate", function(e) { + input.composing.data = e.data; + }); + on(div, "compositionend", function(e) { + var ours = input.composing; + if (!ours) return; + if (e.data != ours.startData && !/\u200b/.test(e.data)) + ours.data = e.data; + // Need a small delay to prevent other code (input event, + // selection polling) from doing damage when fired right after + // compositionend. + setTimeout(function() { + if (!ours.handled) + input.applyComposition(ours); + if (input.composing == ours) + input.composing = null; + }, 50); + }); + + on(div, "touchstart", function() { + input.forceCompositionEnd(); + }); + + on(div, "input", function() { + if (input.composing) return; + if (cm.isReadOnly() || !input.pollContent()) + runInOp(input.cm, function() {regChange(cm);}); + }); + + function onCopyCut(e) { + if (signalDOMEvent(cm, e)) return + if (cm.somethingSelected()) { + lastCopied = {lineWise: false, text: cm.getSelections()}; + if (e.type == "cut") cm.replaceSelection("", null, "cut"); + } else if (!cm.options.lineWiseCopyCut) { + return; + } else { + var ranges = copyableRanges(cm); + lastCopied = {lineWise: true, text: ranges.text}; + if (e.type == "cut") { + cm.operation(function() { + cm.setSelections(ranges.ranges, 0, sel_dontScroll); + cm.replaceSelection("", null, "cut"); + }); + } + } + // iOS exposes the clipboard API, but seems to discard content inserted into it + if (e.clipboardData && !ios) { + e.preventDefault(); + e.clipboardData.clearData(); + e.clipboardData.setData("text/plain", lastCopied.text.join("\n")); + } else { + // Old-fashioned briefly-focus-a-textarea hack + var kludge = hiddenTextarea(), te = kludge.firstChild; + cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild); + te.value = lastCopied.text.join("\n"); + var hadFocus = document.activeElement; + selectInput(te); + setTimeout(function() { + cm.display.lineSpace.removeChild(kludge); + hadFocus.focus(); + }, 50); + } + } + on(div, "copy", onCopyCut); + on(div, "cut", onCopyCut); + }, + + prepareSelection: function() { + var result = prepareSelection(this.cm, false); + result.focus = this.cm.state.focused; + return result; + }, + + showSelection: function(info, takeFocus) { + if (!info || !this.cm.display.view.length) return; + if (info.focus || takeFocus) this.showPrimarySelection(); + this.showMultipleSelections(info); + }, + + showPrimarySelection: function() { + var sel = window.getSelection(), prim = this.cm.doc.sel.primary(); + var curAnchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset); + var curFocus = domToPos(this.cm, sel.focusNode, sel.focusOffset); + if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad && + cmp(minPos(curAnchor, curFocus), prim.from()) == 0 && + cmp(maxPos(curAnchor, curFocus), prim.to()) == 0) + return; + + var start = posToDOM(this.cm, prim.from()); + var end = posToDOM(this.cm, prim.to()); + if (!start && !end) return; + + var view = this.cm.display.view; + var old = sel.rangeCount && sel.getRangeAt(0); + if (!start) { + start = {node: view[0].measure.map[2], offset: 0}; + } else if (!end) { // FIXME dangerously hacky + var measure = view[view.length - 1].measure; + var map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map; + end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]}; + } + + try { var rng = range(start.node, start.offset, end.offset, end.node); } + catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible + if (rng) { + if (!gecko && this.cm.state.focused) { + sel.collapse(start.node, start.offset); + if (!rng.collapsed) sel.addRange(rng); + } else { + sel.removeAllRanges(); + sel.addRange(rng); + } + if (old && sel.anchorNode == null) sel.addRange(old); + else if (gecko) this.startGracePeriod(); + } + this.rememberSelection(); + }, + + startGracePeriod: function() { + var input = this; + clearTimeout(this.gracePeriod); + this.gracePeriod = setTimeout(function() { + input.gracePeriod = false; + if (input.selectionChanged()) + input.cm.operation(function() { input.cm.curOp.selectionChanged = true; }); + }, 20); + }, + + showMultipleSelections: function(info) { + removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors); + removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection); + }, + + rememberSelection: function() { + var sel = window.getSelection(); + this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset; + this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset; + }, + + selectionInEditor: function() { + var sel = window.getSelection(); + if (!sel.rangeCount) return false; + var node = sel.getRangeAt(0).commonAncestorContainer; + return contains(this.div, node); + }, + + focus: function() { + if (this.cm.options.readOnly != "nocursor") this.div.focus(); + }, + blur: function() { this.div.blur(); }, + getField: function() { return this.div; }, + + supportsTouch: function() { return true; }, + + receivedFocus: function() { + var input = this; + if (this.selectionInEditor()) + this.pollSelection(); + else + runInOp(this.cm, function() { input.cm.curOp.selectionChanged = true; }); + + function poll() { + if (input.cm.state.focused) { + input.pollSelection(); + input.polling.set(input.cm.options.pollInterval, poll); + } + } + this.polling.set(this.cm.options.pollInterval, poll); + }, + + selectionChanged: function() { + var sel = window.getSelection(); + return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset || + sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset; + }, + + pollSelection: function() { + if (!this.composing && !this.gracePeriod && this.selectionChanged()) { + var sel = window.getSelection(), cm = this.cm; + this.rememberSelection(); + var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset); + var head = domToPos(cm, sel.focusNode, sel.focusOffset); + if (anchor && head) runInOp(cm, function() { + setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll); + if (anchor.bad || head.bad) cm.curOp.selectionChanged = true; + }); + } + }, + + pollContent: function() { + var cm = this.cm, display = cm.display, sel = cm.doc.sel.primary(); + var from = sel.from(), to = sel.to(); + if (from.line < display.viewFrom || to.line > display.viewTo - 1) return false; + + var fromIndex; + if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) { + var fromLine = lineNo(display.view[0].line); + var fromNode = display.view[0].node; + } else { + var fromLine = lineNo(display.view[fromIndex].line); + var fromNode = display.view[fromIndex - 1].node.nextSibling; + } + var toIndex = findViewIndex(cm, to.line); + if (toIndex == display.view.length - 1) { + var toLine = display.viewTo - 1; + var toNode = display.lineDiv.lastChild; + } else { + var toLine = lineNo(display.view[toIndex + 1].line) - 1; + var toNode = display.view[toIndex + 1].node.previousSibling; + } + + var newText = cm.doc.splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine)); + var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length)); + while (newText.length > 1 && oldText.length > 1) { + if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine--; } + else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++; } + else break; + } + + var cutFront = 0, cutEnd = 0; + var newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length); + while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront)) + ++cutFront; + var newBot = lst(newText), oldBot = lst(oldText); + var maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0), + oldBot.length - (oldText.length == 1 ? cutFront : 0)); + while (cutEnd < maxCutEnd && + newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) + ++cutEnd; + + newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd); + newText[0] = newText[0].slice(cutFront); + + var chFrom = Pos(fromLine, cutFront); + var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0); + if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) { + replaceRange(cm.doc, newText, chFrom, chTo, "+input"); + return true; + } + }, + + ensurePolled: function() { + this.forceCompositionEnd(); + }, + reset: function() { + this.forceCompositionEnd(); + }, + forceCompositionEnd: function() { + if (!this.composing || this.composing.handled) return; + this.applyComposition(this.composing); + this.composing.handled = true; + this.div.blur(); + this.div.focus(); + }, + applyComposition: function(composing) { + if (this.cm.isReadOnly()) + operation(this.cm, regChange)(this.cm) + else if (composing.data && composing.data != composing.startData) + operation(this.cm, applyTextInput)(this.cm, composing.data, 0, composing.sel); + }, + + setUneditable: function(node) { + node.contentEditable = "false" + }, + + onKeyPress: function(e) { + e.preventDefault(); + if (!this.cm.isReadOnly()) + operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0); + }, + + readOnlyChanged: function(val) { + this.div.contentEditable = String(val != "nocursor") + }, + + onContextMenu: nothing, + resetPosition: nothing, + + needsContentAttribute: true + }, ContentEditableInput.prototype); + + function posToDOM(cm, pos) { + var view = findViewForLine(cm, pos.line); + if (!view || view.hidden) return null; + var line = getLine(cm.doc, pos.line); + var info = mapFromLineView(view, line, pos.line); + + var order = getOrder(line), side = "left"; + if (order) { + var partPos = getBidiPartAt(order, pos.ch); + side = partPos % 2 ? "right" : "left"; + } + var result = nodeAndOffsetInLineMap(info.map, pos.ch, side); + result.offset = result.collapse == "right" ? result.end : result.start; + return result; + } + + function badPos(pos, bad) { if (bad) pos.bad = true; return pos; } + + function domToPos(cm, node, offset) { + var lineNode; + if (node == cm.display.lineDiv) { + lineNode = cm.display.lineDiv.childNodes[offset]; + if (!lineNode) return badPos(cm.clipPos(Pos(cm.display.viewTo - 1)), true); + node = null; offset = 0; + } else { + for (lineNode = node;; lineNode = lineNode.parentNode) { + if (!lineNode || lineNode == cm.display.lineDiv) return null; + if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) break; + } + } + for (var i = 0; i < cm.display.view.length; i++) { + var lineView = cm.display.view[i]; + if (lineView.node == lineNode) + return locateNodeInLineView(lineView, node, offset); + } + } + + function locateNodeInLineView(lineView, node, offset) { + var wrapper = lineView.text.firstChild, bad = false; + if (!node || !contains(wrapper, node)) return badPos(Pos(lineNo(lineView.line), 0), true); + if (node == wrapper) { + bad = true; + node = wrapper.childNodes[offset]; + offset = 0; + if (!node) { + var line = lineView.rest ? lst(lineView.rest) : lineView.line; + return badPos(Pos(lineNo(line), line.text.length), bad); + } + } + + var textNode = node.nodeType == 3 ? node : null, topNode = node; + if (!textNode && node.childNodes.length == 1 && node.firstChild.nodeType == 3) { + textNode = node.firstChild; + if (offset) offset = textNode.nodeValue.length; + } + while (topNode.parentNode != wrapper) topNode = topNode.parentNode; + var measure = lineView.measure, maps = measure.maps; + + function find(textNode, topNode, offset) { + for (var i = -1; i < (maps ? maps.length : 0); i++) { + var map = i < 0 ? measure.map : maps[i]; + for (var j = 0; j < map.length; j += 3) { + var curNode = map[j + 2]; + if (curNode == textNode || curNode == topNode) { + var line = lineNo(i < 0 ? lineView.line : lineView.rest[i]); + var ch = map[j] + offset; + if (offset < 0 || curNode != textNode) ch = map[j + (offset ? 1 : 0)]; + return Pos(line, ch); + } + } + } + } + var found = find(textNode, topNode, offset); + if (found) return badPos(found, bad); + + // FIXME this is all really shaky. might handle the few cases it needs to handle, but likely to cause problems + for (var after = topNode.nextSibling, dist = textNode ? textNode.nodeValue.length - offset : 0; after; after = after.nextSibling) { + found = find(after, after.firstChild, 0); + if (found) + return badPos(Pos(found.line, found.ch - dist), bad); + else + dist += after.textContent.length; + } + for (var before = topNode.previousSibling, dist = offset; before; before = before.previousSibling) { + found = find(before, before.firstChild, -1); + if (found) + return badPos(Pos(found.line, found.ch + dist), bad); + else + dist += after.textContent.length; + } + } + + function domTextBetween(cm, from, to, fromLine, toLine) { + var text = "", closing = false, lineSep = cm.doc.lineSeparator(); + function recognizeMarker(id) { return function(marker) { return marker.id == id; }; } + function walk(node) { + if (node.nodeType == 1) { + var cmText = node.getAttribute("cm-text"); + if (cmText != null) { + if (cmText == "") cmText = node.textContent.replace(/\u200b/g, ""); + text += cmText; + return; + } + var markerID = node.getAttribute("cm-marker"), range; + if (markerID) { + var found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID)); + if (found.length && (range = found[0].find())) + text += getBetween(cm.doc, range.from, range.to).join(lineSep); + return; + } + if (node.getAttribute("contenteditable") == "false") return; + for (var i = 0; i < node.childNodes.length; i++) + walk(node.childNodes[i]); + if (/^(pre|div|p)$/i.test(node.nodeName)) + closing = true; + } else if (node.nodeType == 3) { + var val = node.nodeValue; + if (!val) return; + if (closing) { + text += lineSep; + closing = false; + } + text += val; + } + } + for (;;) { + walk(from); + if (from == to) break; + from = from.nextSibling; + } + return text; + } + + CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput}; + + // SELECTION / CURSOR + + // Selection objects are immutable. A new one is created every time + // the selection changes. A selection is one or more non-overlapping + // (and non-touching) ranges, sorted, and an integer that indicates + // which one is the primary selection (the one that's scrolled into + // view, that getCursor returns, etc). + function Selection(ranges, primIndex) { + this.ranges = ranges; + this.primIndex = primIndex; + } + + Selection.prototype = { + primary: function() { return this.ranges[this.primIndex]; }, + equals: function(other) { + if (other == this) return true; + if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) return false; + for (var i = 0; i < this.ranges.length; i++) { + var here = this.ranges[i], there = other.ranges[i]; + if (cmp(here.anchor, there.anchor) != 0 || cmp(here.head, there.head) != 0) return false; + } + return true; + }, + deepCopy: function() { + for (var out = [], i = 0; i < this.ranges.length; i++) + out[i] = new Range(copyPos(this.ranges[i].anchor), copyPos(this.ranges[i].head)); + return new Selection(out, this.primIndex); + }, + somethingSelected: function() { + for (var i = 0; i < this.ranges.length; i++) + if (!this.ranges[i].empty()) return true; + return false; + }, + contains: function(pos, end) { + if (!end) end = pos; + for (var i = 0; i < this.ranges.length; i++) { + var range = this.ranges[i]; + if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0) + return i; + } + return -1; + } + }; + + function Range(anchor, head) { + this.anchor = anchor; this.head = head; + } + + Range.prototype = { + from: function() { return minPos(this.anchor, this.head); }, + to: function() { return maxPos(this.anchor, this.head); }, + empty: function() { + return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch; + } + }; + + // Take an unsorted, potentially overlapping set of ranges, and + // build a selection out of it. 'Consumes' ranges array (modifying + // it). + function normalizeSelection(ranges, primIndex) { + var prim = ranges[primIndex]; + ranges.sort(function(a, b) { return cmp(a.from(), b.from()); }); + primIndex = indexOf(ranges, prim); + for (var i = 1; i < ranges.length; i++) { + var cur = ranges[i], prev = ranges[i - 1]; + if (cmp(prev.to(), cur.from()) >= 0) { + var from = minPos(prev.from(), cur.from()), to = maxPos(prev.to(), cur.to()); + var inv = prev.empty() ? cur.from() == cur.head : prev.from() == prev.head; + if (i <= primIndex) --primIndex; + ranges.splice(--i, 2, new Range(inv ? to : from, inv ? from : to)); + } + } + return new Selection(ranges, primIndex); + } + + function simpleSelection(anchor, head) { + return new Selection([new Range(anchor, head || anchor)], 0); + } + + // Most of the external API clips given positions to make sure they + // actually exist within the document. + function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1));} + function clipPos(doc, pos) { + if (pos.line < doc.first) return Pos(doc.first, 0); + var last = doc.first + doc.size - 1; + if (pos.line > last) return Pos(last, getLine(doc, last).text.length); + return clipToLen(pos, getLine(doc, pos.line).text.length); + } + function clipToLen(pos, linelen) { + var ch = pos.ch; + if (ch == null || ch > linelen) return Pos(pos.line, linelen); + else if (ch < 0) return Pos(pos.line, 0); + else return pos; + } + function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size;} + function clipPosArray(doc, array) { + for (var out = [], i = 0; i < array.length; i++) out[i] = clipPos(doc, array[i]); + return out; + } + + // SELECTION UPDATES + + // The 'scroll' parameter given to many of these indicated whether + // the new cursor position should be scrolled into view after + // modifying the selection. + + // If shift is held or the extend flag is set, extends a range to + // include a given position (and optionally a second position). + // Otherwise, simply returns the range between the given positions. + // Used for cursor motion and such. + function extendRange(doc, range, head, other) { + if (doc.cm && doc.cm.display.shift || doc.extend) { + var anchor = range.anchor; + if (other) { + var posBefore = cmp(head, anchor) < 0; + if (posBefore != (cmp(other, anchor) < 0)) { + anchor = head; + head = other; + } else if (posBefore != (cmp(head, other) < 0)) { + head = other; + } + } + return new Range(anchor, head); + } else { + return new Range(other || head, head); + } + } + + // Extend the primary selection range, discard the rest. + function extendSelection(doc, head, other, options) { + setSelection(doc, new Selection([extendRange(doc, doc.sel.primary(), head, other)], 0), options); + } + + // Extend all selections (pos is an array of selections with length + // equal the number of selections) + function extendSelections(doc, heads, options) { + for (var out = [], i = 0; i < doc.sel.ranges.length; i++) + out[i] = extendRange(doc, doc.sel.ranges[i], heads[i], null); + var newSel = normalizeSelection(out, doc.sel.primIndex); + setSelection(doc, newSel, options); + } + + // Updates a single range in the selection. + function replaceOneSelection(doc, i, range, options) { + var ranges = doc.sel.ranges.slice(0); + ranges[i] = range; + setSelection(doc, normalizeSelection(ranges, doc.sel.primIndex), options); + } + + // Reset the selection to a single range. + function setSimpleSelection(doc, anchor, head, options) { + setSelection(doc, simpleSelection(anchor, head), options); + } + + // Give beforeSelectionChange handlers a change to influence a + // selection update. + function filterSelectionChange(doc, sel, options) { + var obj = { + ranges: sel.ranges, + update: function(ranges) { + this.ranges = []; + for (var i = 0; i < ranges.length; i++) + this.ranges[i] = new Range(clipPos(doc, ranges[i].anchor), + clipPos(doc, ranges[i].head)); + }, + origin: options && options.origin + }; + signal(doc, "beforeSelectionChange", doc, obj); + if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj); + if (obj.ranges != sel.ranges) return normalizeSelection(obj.ranges, obj.ranges.length - 1); + else return sel; + } + + function setSelectionReplaceHistory(doc, sel, options) { + var done = doc.history.done, last = lst(done); + if (last && last.ranges) { + done[done.length - 1] = sel; + setSelectionNoUndo(doc, sel, options); + } else { + setSelection(doc, sel, options); + } + } + + // Set a new selection. + function setSelection(doc, sel, options) { + setSelectionNoUndo(doc, sel, options); + addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options); + } + + function setSelectionNoUndo(doc, sel, options) { + if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) + sel = filterSelectionChange(doc, sel, options); + + var bias = options && options.bias || + (cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1); + setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true)); + + if (!(options && options.scroll === false) && doc.cm) + ensureCursorVisible(doc.cm); + } + + function setSelectionInner(doc, sel) { + if (sel.equals(doc.sel)) return; + + doc.sel = sel; + + if (doc.cm) { + doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = true; + signalCursorActivity(doc.cm); + } + signalLater(doc, "cursorActivity", doc); + } + + // Verify that the selection does not partially select any atomic + // marked ranges. + function reCheckSelection(doc) { + setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false), sel_dontScroll); + } + + // Return a selection that does not partially select any atomic + // ranges. + function skipAtomicInSelection(doc, sel, bias, mayClear) { + var out; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + var old = sel.ranges.length == doc.sel.ranges.length && doc.sel.ranges[i]; + var newAnchor = skipAtomic(doc, range.anchor, old && old.anchor, bias, mayClear); + var newHead = skipAtomic(doc, range.head, old && old.head, bias, mayClear); + if (out || newAnchor != range.anchor || newHead != range.head) { + if (!out) out = sel.ranges.slice(0, i); + out[i] = new Range(newAnchor, newHead); + } + } + return out ? normalizeSelection(out, sel.primIndex) : sel; + } + + function skipAtomicInner(doc, pos, oldPos, dir, mayClear) { + var line = getLine(doc, pos.line); + if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) { + var sp = line.markedSpans[i], m = sp.marker; + if ((sp.from == null || (m.inclusiveLeft ? sp.from <= pos.ch : sp.from < pos.ch)) && + (sp.to == null || (m.inclusiveRight ? sp.to >= pos.ch : sp.to > pos.ch))) { + if (mayClear) { + signal(m, "beforeCursorEnter"); + if (m.explicitlyCleared) { + if (!line.markedSpans) break; + else {--i; continue;} + } + } + if (!m.atomic) continue; + + if (oldPos) { + var near = m.find(dir < 0 ? 1 : -1), diff; + if (dir < 0 ? m.inclusiveRight : m.inclusiveLeft) + near = movePos(doc, near, -dir, near && near.line == pos.line ? line : null); + if (near && near.line == pos.line && (diff = cmp(near, oldPos)) && (dir < 0 ? diff < 0 : diff > 0)) + return skipAtomicInner(doc, near, pos, dir, mayClear); + } + + var far = m.find(dir < 0 ? -1 : 1); + if (dir < 0 ? m.inclusiveLeft : m.inclusiveRight) + far = movePos(doc, far, dir, far.line == pos.line ? line : null); + return far ? skipAtomicInner(doc, far, pos, dir, mayClear) : null; + } + } + return pos; + } + + // Ensure a given position is not inside an atomic range. + function skipAtomic(doc, pos, oldPos, bias, mayClear) { + var dir = bias || 1; + var found = skipAtomicInner(doc, pos, oldPos, dir, mayClear) || + (!mayClear && skipAtomicInner(doc, pos, oldPos, dir, true)) || + skipAtomicInner(doc, pos, oldPos, -dir, mayClear) || + (!mayClear && skipAtomicInner(doc, pos, oldPos, -dir, true)); + if (!found) { + doc.cantEdit = true; + return Pos(doc.first, 0); + } + return found; + } + + function movePos(doc, pos, dir, line) { + if (dir < 0 && pos.ch == 0) { + if (pos.line > doc.first) return clipPos(doc, Pos(pos.line - 1)); + else return null; + } else if (dir > 0 && pos.ch == (line || getLine(doc, pos.line)).text.length) { + if (pos.line < doc.first + doc.size - 1) return Pos(pos.line + 1, 0); + else return null; + } else { + return new Pos(pos.line, pos.ch + dir); + } + } + + // SELECTION DRAWING + + function updateSelection(cm) { + cm.display.input.showSelection(cm.display.input.prepareSelection()); + } + + function prepareSelection(cm, primary) { + var doc = cm.doc, result = {}; + var curFragment = result.cursors = document.createDocumentFragment(); + var selFragment = result.selection = document.createDocumentFragment(); + + for (var i = 0; i < doc.sel.ranges.length; i++) { + if (primary === false && i == doc.sel.primIndex) continue; + var range = doc.sel.ranges[i]; + if (range.from().line >= cm.display.viewTo || range.to().line < cm.display.viewFrom) continue; + var collapsed = range.empty(); + if (collapsed || cm.options.showCursorWhenSelecting) + drawSelectionCursor(cm, range.head, curFragment); + if (!collapsed) + drawSelectionRange(cm, range, selFragment); + } + return result; + } + + // Draws a cursor for the given range + function drawSelectionCursor(cm, head, output) { + var pos = cursorCoords(cm, head, "div", null, null, !cm.options.singleCursorHeightPerLine); + + var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor")); + cursor.style.left = pos.left + "px"; + cursor.style.top = pos.top + "px"; + cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px"; + + if (pos.other) { + // Secondary cursor, shown when on a 'jump' in bi-directional text + var otherCursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor")); + otherCursor.style.display = ""; + otherCursor.style.left = pos.other.left + "px"; + otherCursor.style.top = pos.other.top + "px"; + otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px"; + } + } + + // Draws the given range as a highlighted selection + function drawSelectionRange(cm, range, output) { + var display = cm.display, doc = cm.doc; + var fragment = document.createDocumentFragment(); + var padding = paddingH(cm.display), leftSide = padding.left; + var rightSide = Math.max(display.sizerWidth, displayWidth(cm) - display.sizer.offsetLeft) - padding.right; + + function add(left, top, width, bottom) { + if (top < 0) top = 0; + top = Math.round(top); + bottom = Math.round(bottom); + fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left + + "px; top: " + top + "px; width: " + (width == null ? rightSide - left : width) + + "px; height: " + (bottom - top) + "px")); + } + + function drawForLine(line, fromArg, toArg) { + var lineObj = getLine(doc, line); + var lineLen = lineObj.text.length; + var start, end; + function coords(ch, bias) { + return charCoords(cm, Pos(line, ch), "div", lineObj, bias); + } + + iterateBidiSections(getOrder(lineObj), fromArg || 0, toArg == null ? lineLen : toArg, function(from, to, dir) { + var leftPos = coords(from, "left"), rightPos, left, right; + if (from == to) { + rightPos = leftPos; + left = right = leftPos.left; + } else { + rightPos = coords(to - 1, "right"); + if (dir == "rtl") { var tmp = leftPos; leftPos = rightPos; rightPos = tmp; } + left = leftPos.left; + right = rightPos.right; + } + if (fromArg == null && from == 0) left = leftSide; + if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part + add(left, leftPos.top, null, leftPos.bottom); + left = leftSide; + if (leftPos.bottom < rightPos.top) add(left, leftPos.bottom, null, rightPos.top); + } + if (toArg == null && to == lineLen) right = rightSide; + if (!start || leftPos.top < start.top || leftPos.top == start.top && leftPos.left < start.left) + start = leftPos; + if (!end || rightPos.bottom > end.bottom || rightPos.bottom == end.bottom && rightPos.right > end.right) + end = rightPos; + if (left < leftSide + 1) left = leftSide; + add(left, rightPos.top, right - left, rightPos.bottom); + }); + return {start: start, end: end}; + } + + var sFrom = range.from(), sTo = range.to(); + if (sFrom.line == sTo.line) { + drawForLine(sFrom.line, sFrom.ch, sTo.ch); + } else { + var fromLine = getLine(doc, sFrom.line), toLine = getLine(doc, sTo.line); + var singleVLine = visualLine(fromLine) == visualLine(toLine); + var leftEnd = drawForLine(sFrom.line, sFrom.ch, singleVLine ? fromLine.text.length + 1 : null).end; + var rightStart = drawForLine(sTo.line, singleVLine ? 0 : null, sTo.ch).start; + if (singleVLine) { + if (leftEnd.top < rightStart.top - 2) { + add(leftEnd.right, leftEnd.top, null, leftEnd.bottom); + add(leftSide, rightStart.top, rightStart.left, rightStart.bottom); + } else { + add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom); + } + } + if (leftEnd.bottom < rightStart.top) + add(leftSide, leftEnd.bottom, null, rightStart.top); + } + + output.appendChild(fragment); + } + + // Cursor-blinking + function restartBlink(cm) { + if (!cm.state.focused) return; + var display = cm.display; + clearInterval(display.blinker); + var on = true; + display.cursorDiv.style.visibility = ""; + if (cm.options.cursorBlinkRate > 0) + display.blinker = setInterval(function() { + display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden"; + }, cm.options.cursorBlinkRate); + else if (cm.options.cursorBlinkRate < 0) + display.cursorDiv.style.visibility = "hidden"; + } + + // HIGHLIGHT WORKER + + function startWorker(cm, time) { + if (cm.doc.mode.startState && cm.doc.frontier < cm.display.viewTo) + cm.state.highlight.set(time, bind(highlightWorker, cm)); + } + + function highlightWorker(cm) { + var doc = cm.doc; + if (doc.frontier < doc.first) doc.frontier = doc.first; + if (doc.frontier >= cm.display.viewTo) return; + var end = +new Date + cm.options.workTime; + var state = copyState(doc.mode, getStateBefore(cm, doc.frontier)); + var changedLines = []; + + doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function(line) { + if (doc.frontier >= cm.display.viewFrom) { // Visible + var oldStyles = line.styles, tooLong = line.text.length > cm.options.maxHighlightLength; + var highlighted = highlightLine(cm, line, tooLong ? copyState(doc.mode, state) : state, true); + line.styles = highlighted.styles; + var oldCls = line.styleClasses, newCls = highlighted.classes; + if (newCls) line.styleClasses = newCls; + else if (oldCls) line.styleClasses = null; + var ischange = !oldStyles || oldStyles.length != line.styles.length || + oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass); + for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i]; + if (ischange) changedLines.push(doc.frontier); + line.stateAfter = tooLong ? state : copyState(doc.mode, state); + } else { + if (line.text.length <= cm.options.maxHighlightLength) + processLine(cm, line.text, state); + line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null; + } + ++doc.frontier; + if (+new Date > end) { + startWorker(cm, cm.options.workDelay); + return true; + } + }); + if (changedLines.length) runInOp(cm, function() { + for (var i = 0; i < changedLines.length; i++) + regLineChange(cm, changedLines[i], "text"); + }); + } + + // Finds the line to start with when starting a parse. Tries to + // find a line with a stateAfter, so that it can start with a + // valid state. If that fails, it returns the line with the + // smallest indentation, which tends to need the least context to + // parse correctly. + function findStartLine(cm, n, precise) { + var minindent, minline, doc = cm.doc; + var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100); + for (var search = n; search > lim; --search) { + if (search <= doc.first) return doc.first; + var line = getLine(doc, search - 1); + if (line.stateAfter && (!precise || search <= doc.frontier)) return search; + var indented = countColumn(line.text, null, cm.options.tabSize); + if (minline == null || minindent > indented) { + minline = search - 1; + minindent = indented; + } + } + return minline; + } + + function getStateBefore(cm, n, precise) { + var doc = cm.doc, display = cm.display; + if (!doc.mode.startState) return true; + var pos = findStartLine(cm, n, precise), state = pos > doc.first && getLine(doc, pos-1).stateAfter; + if (!state) state = startState(doc.mode); + else state = copyState(doc.mode, state); + doc.iter(pos, n, function(line) { + processLine(cm, line.text, state); + var save = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo; + line.stateAfter = save ? copyState(doc.mode, state) : null; + ++pos; + }); + if (precise) doc.frontier = pos; + return state; + } + + // POSITION MEASUREMENT + + function paddingTop(display) {return display.lineSpace.offsetTop;} + function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight;} + function paddingH(display) { + if (display.cachedPaddingH) return display.cachedPaddingH; + var e = removeChildrenAndAdd(display.measure, elt("pre", "x")); + var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle; + var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)}; + if (!isNaN(data.left) && !isNaN(data.right)) display.cachedPaddingH = data; + return data; + } + + function scrollGap(cm) { return scrollerGap - cm.display.nativeBarWidth; } + function displayWidth(cm) { + return cm.display.scroller.clientWidth - scrollGap(cm) - cm.display.barWidth; + } + function displayHeight(cm) { + return cm.display.scroller.clientHeight - scrollGap(cm) - cm.display.barHeight; + } + + // Ensure the lineView.wrapping.heights array is populated. This is + // an array of bottom offsets for the lines that make up a drawn + // line. When lineWrapping is on, there might be more than one + // height. + function ensureLineHeights(cm, lineView, rect) { + var wrapping = cm.options.lineWrapping; + var curWidth = wrapping && displayWidth(cm); + if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) { + var heights = lineView.measure.heights = []; + if (wrapping) { + lineView.measure.width = curWidth; + var rects = lineView.text.firstChild.getClientRects(); + for (var i = 0; i < rects.length - 1; i++) { + var cur = rects[i], next = rects[i + 1]; + if (Math.abs(cur.bottom - next.bottom) > 2) + heights.push((cur.bottom + next.top) / 2 - rect.top); + } + } + heights.push(rect.bottom - rect.top); + } + } + + // Find a line map (mapping character offsets to text nodes) and a + // measurement cache for the given line number. (A line view might + // contain multiple lines when collapsed ranges are present.) + function mapFromLineView(lineView, line, lineN) { + if (lineView.line == line) + return {map: lineView.measure.map, cache: lineView.measure.cache}; + for (var i = 0; i < lineView.rest.length; i++) + if (lineView.rest[i] == line) + return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]}; + for (var i = 0; i < lineView.rest.length; i++) + if (lineNo(lineView.rest[i]) > lineN) + return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i], before: true}; + } + + // Render a line into the hidden node display.externalMeasured. Used + // when measurement is needed for a line that's not in the viewport. + function updateExternalMeasurement(cm, line) { + line = visualLine(line); + var lineN = lineNo(line); + var view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN); + view.lineN = lineN; + var built = view.built = buildLineContent(cm, view); + view.text = built.pre; + removeChildrenAndAdd(cm.display.lineMeasure, built.pre); + return view; + } + + // Get a {top, bottom, left, right} box (in line-local coordinates) + // for a given character. + function measureChar(cm, line, ch, bias) { + return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias); + } + + // Find a line view that corresponds to the given line number. + function findViewForLine(cm, lineN) { + if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo) + return cm.display.view[findViewIndex(cm, lineN)]; + var ext = cm.display.externalMeasured; + if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size) + return ext; + } + + // Measurement can be split in two steps, the set-up work that + // applies to the whole line, and the measurement of the actual + // character. Functions like coordsChar, that need to do a lot of + // measurements in a row, can thus ensure that the set-up work is + // only done once. + function prepareMeasureForLine(cm, line) { + var lineN = lineNo(line); + var view = findViewForLine(cm, lineN); + if (view && !view.text) { + view = null; + } else if (view && view.changes) { + updateLineForChanges(cm, view, lineN, getDimensions(cm)); + cm.curOp.forceUpdate = true; + } + if (!view) + view = updateExternalMeasurement(cm, line); + + var info = mapFromLineView(view, line, lineN); + return { + line: line, view: view, rect: null, + map: info.map, cache: info.cache, before: info.before, + hasHeights: false + }; + } + + // Given a prepared measurement object, measures the position of an + // actual character (or fetches it from the cache). + function measureCharPrepared(cm, prepared, ch, bias, varHeight) { + if (prepared.before) ch = -1; + var key = ch + (bias || ""), found; + if (prepared.cache.hasOwnProperty(key)) { + found = prepared.cache[key]; + } else { + if (!prepared.rect) + prepared.rect = prepared.view.text.getBoundingClientRect(); + if (!prepared.hasHeights) { + ensureLineHeights(cm, prepared.view, prepared.rect); + prepared.hasHeights = true; + } + found = measureCharInner(cm, prepared, ch, bias); + if (!found.bogus) prepared.cache[key] = found; + } + return {left: found.left, right: found.right, + top: varHeight ? found.rtop : found.top, + bottom: varHeight ? found.rbottom : found.bottom}; + } + + var nullRect = {left: 0, right: 0, top: 0, bottom: 0}; + + function nodeAndOffsetInLineMap(map, ch, bias) { + var node, start, end, collapse; + // First, search the line map for the text node corresponding to, + // or closest to, the target character. + for (var i = 0; i < map.length; i += 3) { + var mStart = map[i], mEnd = map[i + 1]; + if (ch < mStart) { + start = 0; end = 1; + collapse = "left"; + } else if (ch < mEnd) { + start = ch - mStart; + end = start + 1; + } else if (i == map.length - 3 || ch == mEnd && map[i + 3] > ch) { + end = mEnd - mStart; + start = end - 1; + if (ch >= mEnd) collapse = "right"; + } + if (start != null) { + node = map[i + 2]; + if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right")) + collapse = bias; + if (bias == "left" && start == 0) + while (i && map[i - 2] == map[i - 3] && map[i - 1].insertLeft) { + node = map[(i -= 3) + 2]; + collapse = "left"; + } + if (bias == "right" && start == mEnd - mStart) + while (i < map.length - 3 && map[i + 3] == map[i + 4] && !map[i + 5].insertLeft) { + node = map[(i += 3) + 2]; + collapse = "right"; + } + break; + } + } + return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd}; + } + + function measureCharInner(cm, prepared, ch, bias) { + var place = nodeAndOffsetInLineMap(prepared.map, ch, bias); + var node = place.node, start = place.start, end = place.end, collapse = place.collapse; + + var rect; + if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates. + for (var i = 0; i < 4; i++) { // Retry a maximum of 4 times when nonsense rectangles are returned + while (start && isExtendingChar(prepared.line.text.charAt(place.coverStart + start))) --start; + while (place.coverStart + end < place.coverEnd && isExtendingChar(prepared.line.text.charAt(place.coverStart + end))) ++end; + if (ie && ie_version < 9 && start == 0 && end == place.coverEnd - place.coverStart) { + rect = node.parentNode.getBoundingClientRect(); + } else if (ie && cm.options.lineWrapping) { + var rects = range(node, start, end).getClientRects(); + if (rects.length) + rect = rects[bias == "right" ? rects.length - 1 : 0]; + else + rect = nullRect; + } else { + rect = range(node, start, end).getBoundingClientRect() || nullRect; + } + if (rect.left || rect.right || start == 0) break; + end = start; + start = start - 1; + collapse = "right"; + } + if (ie && ie_version < 11) rect = maybeUpdateRectForZooming(cm.display.measure, rect); + } else { // If it is a widget, simply get the box for the whole widget. + if (start > 0) collapse = bias = "right"; + var rects; + if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1) + rect = rects[bias == "right" ? rects.length - 1 : 0]; + else + rect = node.getBoundingClientRect(); + } + if (ie && ie_version < 9 && !start && (!rect || !rect.left && !rect.right)) { + var rSpan = node.parentNode.getClientRects()[0]; + if (rSpan) + rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom}; + else + rect = nullRect; + } + + var rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top; + var mid = (rtop + rbot) / 2; + var heights = prepared.view.measure.heights; + for (var i = 0; i < heights.length - 1; i++) + if (mid < heights[i]) break; + var top = i ? heights[i - 1] : 0, bot = heights[i]; + var result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left, + right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left, + top: top, bottom: bot}; + if (!rect.left && !rect.right) result.bogus = true; + if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot; } + + return result; + } + + // Work around problem with bounding client rects on ranges being + // returned incorrectly when zoomed on IE10 and below. + function maybeUpdateRectForZooming(measure, rect) { + if (!window.screen || screen.logicalXDPI == null || + screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure)) + return rect; + var scaleX = screen.logicalXDPI / screen.deviceXDPI; + var scaleY = screen.logicalYDPI / screen.deviceYDPI; + return {left: rect.left * scaleX, right: rect.right * scaleX, + top: rect.top * scaleY, bottom: rect.bottom * scaleY}; + } + + function clearLineMeasurementCacheFor(lineView) { + if (lineView.measure) { + lineView.measure.cache = {}; + lineView.measure.heights = null; + if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++) + lineView.measure.caches[i] = {}; + } + } + + function clearLineMeasurementCache(cm) { + cm.display.externalMeasure = null; + removeChildren(cm.display.lineMeasure); + for (var i = 0; i < cm.display.view.length; i++) + clearLineMeasurementCacheFor(cm.display.view[i]); + } + + function clearCaches(cm) { + clearLineMeasurementCache(cm); + cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null; + if (!cm.options.lineWrapping) cm.display.maxLineChanged = true; + cm.display.lineNumChars = null; + } + + function pageScrollX() { return window.pageXOffset || (document.documentElement || document.body).scrollLeft; } + function pageScrollY() { return window.pageYOffset || (document.documentElement || document.body).scrollTop; } + + // Converts a {top, bottom, left, right} box from line-local + // coordinates into another coordinate system. Context may be one of + // "line", "div" (display.lineDiv), "local"/null (editor), "window", + // or "page". + function intoCoordSystem(cm, lineObj, rect, context) { + if (lineObj.widgets) for (var i = 0; i < lineObj.widgets.length; ++i) if (lineObj.widgets[i].above) { + var size = widgetHeight(lineObj.widgets[i]); + rect.top += size; rect.bottom += size; + } + if (context == "line") return rect; + if (!context) context = "local"; + var yOff = heightAtLine(lineObj); + if (context == "local") yOff += paddingTop(cm.display); + else yOff -= cm.display.viewOffset; + if (context == "page" || context == "window") { + var lOff = cm.display.lineSpace.getBoundingClientRect(); + yOff += lOff.top + (context == "window" ? 0 : pageScrollY()); + var xOff = lOff.left + (context == "window" ? 0 : pageScrollX()); + rect.left += xOff; rect.right += xOff; + } + rect.top += yOff; rect.bottom += yOff; + return rect; + } + + // Coverts a box from "div" coords to another coordinate system. + // Context may be "window", "page", "div", or "local"/null. + function fromCoordSystem(cm, coords, context) { + if (context == "div") return coords; + var left = coords.left, top = coords.top; + // First move into "page" coordinate system + if (context == "page") { + left -= pageScrollX(); + top -= pageScrollY(); + } else if (context == "local" || !context) { + var localBox = cm.display.sizer.getBoundingClientRect(); + left += localBox.left; + top += localBox.top; + } + + var lineSpaceBox = cm.display.lineSpace.getBoundingClientRect(); + return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top}; + } + + function charCoords(cm, pos, context, lineObj, bias) { + if (!lineObj) lineObj = getLine(cm.doc, pos.line); + return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context); + } + + // Returns a box for a given cursor position, which may have an + // 'other' property containing the position of the secondary cursor + // on a bidi boundary. + function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) { + lineObj = lineObj || getLine(cm.doc, pos.line); + if (!preparedMeasure) preparedMeasure = prepareMeasureForLine(cm, lineObj); + function get(ch, right) { + var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight); + if (right) m.left = m.right; else m.right = m.left; + return intoCoordSystem(cm, lineObj, m, context); + } + function getBidi(ch, partPos) { + var part = order[partPos], right = part.level % 2; + if (ch == bidiLeft(part) && partPos && part.level < order[partPos - 1].level) { + part = order[--partPos]; + ch = bidiRight(part) - (part.level % 2 ? 0 : 1); + right = true; + } else if (ch == bidiRight(part) && partPos < order.length - 1 && part.level < order[partPos + 1].level) { + part = order[++partPos]; + ch = bidiLeft(part) - part.level % 2; + right = false; + } + if (right && ch == part.to && ch > part.from) return get(ch - 1); + return get(ch, right); + } + var order = getOrder(lineObj), ch = pos.ch; + if (!order) return get(ch); + var partPos = getBidiPartAt(order, ch); + var val = getBidi(ch, partPos); + if (bidiOther != null) val.other = getBidi(ch, bidiOther); + return val; + } + + // Used to cheaply estimate the coordinates for a position. Used for + // intermediate scroll updates. + function estimateCoords(cm, pos) { + var left = 0, pos = clipPos(cm.doc, pos); + if (!cm.options.lineWrapping) left = charWidth(cm.display) * pos.ch; + var lineObj = getLine(cm.doc, pos.line); + var top = heightAtLine(lineObj) + paddingTop(cm.display); + return {left: left, right: left, top: top, bottom: top + lineObj.height}; + } + + // Positions returned by coordsChar contain some extra information. + // xRel is the relative x position of the input coordinates compared + // to the found position (so xRel > 0 means the coordinates are to + // the right of the character position, for example). When outside + // is true, that means the coordinates lie outside the line's + // vertical range. + function PosWithInfo(line, ch, outside, xRel) { + var pos = Pos(line, ch); + pos.xRel = xRel; + if (outside) pos.outside = true; + return pos; + } + + // Compute the character position closest to the given coordinates. + // Input must be lineSpace-local ("div" coordinate system). + function coordsChar(cm, x, y) { + var doc = cm.doc; + y += cm.display.viewOffset; + if (y < 0) return PosWithInfo(doc.first, 0, true, -1); + var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1; + if (lineN > last) + return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, true, 1); + if (x < 0) x = 0; + + var lineObj = getLine(doc, lineN); + for (;;) { + var found = coordsCharInner(cm, lineObj, lineN, x, y); + var merged = collapsedSpanAtEnd(lineObj); + var mergedPos = merged && merged.find(0, true); + if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0)) + lineN = lineNo(lineObj = mergedPos.to.line); + else + return found; + } + } + + function coordsCharInner(cm, lineObj, lineNo, x, y) { + var innerOff = y - heightAtLine(lineObj); + var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth; + var preparedMeasure = prepareMeasureForLine(cm, lineObj); + + function getX(ch) { + var sp = cursorCoords(cm, Pos(lineNo, ch), "line", lineObj, preparedMeasure); + wrongLine = true; + if (innerOff > sp.bottom) return sp.left - adjust; + else if (innerOff < sp.top) return sp.left + adjust; + else wrongLine = false; + return sp.left; + } + + var bidi = getOrder(lineObj), dist = lineObj.text.length; + var from = lineLeft(lineObj), to = lineRight(lineObj); + var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine; + + if (x > toX) return PosWithInfo(lineNo, to, toOutside, 1); + // Do a binary search between these bounds. + for (;;) { + if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) { + var ch = x < fromX || x - fromX <= toX - x ? from : to; + var outside = ch == from ? fromOutside : toOutside + var xDiff = x - (ch == from ? fromX : toX); + // This is a kludge to handle the case where the coordinates + // are after a line-wrapped line. We should replace it with a + // more general handling of cursor positions around line + // breaks. (Issue #4078) + if (toOutside && !bidi && !/\s/.test(lineObj.text.charAt(ch)) && xDiff > 0 && + ch < lineObj.text.length && preparedMeasure.view.measure.heights.length > 1) { + var charSize = measureCharPrepared(cm, preparedMeasure, ch, "right"); + if (innerOff <= charSize.bottom && innerOff >= charSize.top && Math.abs(x - charSize.right) < xDiff) { + outside = false + ch++ + xDiff = x - charSize.right + } + } + while (isExtendingChar(lineObj.text.charAt(ch))) ++ch; + var pos = PosWithInfo(lineNo, ch, outside, xDiff < -1 ? -1 : xDiff > 1 ? 1 : 0); + return pos; + } + var step = Math.ceil(dist / 2), middle = from + step; + if (bidi) { + middle = from; + for (var i = 0; i < step; ++i) middle = moveVisually(lineObj, middle, 1); + } + var middleX = getX(middle); + if (middleX > x) {to = middle; toX = middleX; if (toOutside = wrongLine) toX += 1000; dist = step;} + else {from = middle; fromX = middleX; fromOutside = wrongLine; dist -= step;} + } + } + + var measureText; + // Compute the default text height. + function textHeight(display) { + if (display.cachedTextHeight != null) return display.cachedTextHeight; + if (measureText == null) { + measureText = elt("pre"); + // Measure a bunch of lines, for browsers that compute + // fractional heights. + for (var i = 0; i < 49; ++i) { + measureText.appendChild(document.createTextNode("x")); + measureText.appendChild(elt("br")); + } + measureText.appendChild(document.createTextNode("x")); + } + removeChildrenAndAdd(display.measure, measureText); + var height = measureText.offsetHeight / 50; + if (height > 3) display.cachedTextHeight = height; + removeChildren(display.measure); + return height || 1; + } + + // Compute the default character width. + function charWidth(display) { + if (display.cachedCharWidth != null) return display.cachedCharWidth; + var anchor = elt("span", "xxxxxxxxxx"); + var pre = elt("pre", [anchor]); + removeChildrenAndAdd(display.measure, pre); + var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10; + if (width > 2) display.cachedCharWidth = width; + return width || 10; + } + + // OPERATIONS + + // Operations are used to wrap a series of changes to the editor + // state in such a way that each change won't have to update the + // cursor and display (which would be awkward, slow, and + // error-prone). Instead, display updates are batched and then all + // combined and executed at once. + + var operationGroup = null; + + var nextOpId = 0; + // Start a new operation. + function startOperation(cm) { + cm.curOp = { + cm: cm, + viewChanged: false, // Flag that indicates that lines might need to be redrawn + startHeight: cm.doc.height, // Used to detect need to update scrollbar + forceUpdate: false, // Used to force a redraw + updateInput: null, // Whether to reset the input textarea + typing: false, // Whether this reset should be careful to leave existing text (for compositing) + changeObjs: null, // Accumulated changes, for firing change events + cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on + cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already + selectionChanged: false, // Whether the selection needs to be redrawn + updateMaxLine: false, // Set when the widest line needs to be determined anew + scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet + scrollToPos: null, // Used to scroll to a specific position + focus: false, + id: ++nextOpId // Unique ID + }; + if (operationGroup) { + operationGroup.ops.push(cm.curOp); + } else { + cm.curOp.ownsGroup = operationGroup = { + ops: [cm.curOp], + delayedCallbacks: [] + }; + } + } + + function fireCallbacksForOps(group) { + // Calls delayed callbacks and cursorActivity handlers until no + // new ones appear + var callbacks = group.delayedCallbacks, i = 0; + do { + for (; i < callbacks.length; i++) + callbacks[i].call(null); + for (var j = 0; j < group.ops.length; j++) { + var op = group.ops[j]; + if (op.cursorActivityHandlers) + while (op.cursorActivityCalled < op.cursorActivityHandlers.length) + op.cursorActivityHandlers[op.cursorActivityCalled++].call(null, op.cm); + } + } while (i < callbacks.length); + } + + // Finish an operation, updating the display and signalling delayed events + function endOperation(cm) { + var op = cm.curOp, group = op.ownsGroup; + if (!group) return; + + try { fireCallbacksForOps(group); } + finally { + operationGroup = null; + for (var i = 0; i < group.ops.length; i++) + group.ops[i].cm.curOp = null; + endOperations(group); + } + } + + // The DOM updates done when an operation finishes are batched so + // that the minimum number of relayouts are required. + function endOperations(group) { + var ops = group.ops; + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_R1(ops[i]); + for (var i = 0; i < ops.length; i++) // Write DOM (maybe) + endOperation_W1(ops[i]); + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_R2(ops[i]); + for (var i = 0; i < ops.length; i++) // Write DOM (maybe) + endOperation_W2(ops[i]); + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_finish(ops[i]); + } + + function endOperation_R1(op) { + var cm = op.cm, display = cm.display; + maybeClipScrollbars(cm); + if (op.updateMaxLine) findMaxLine(cm); + + op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null || + op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom || + op.scrollToPos.to.line >= display.viewTo) || + display.maxLineChanged && cm.options.lineWrapping; + op.update = op.mustUpdate && + new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate); + } + + function endOperation_W1(op) { + op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update); + } + + function endOperation_R2(op) { + var cm = op.cm, display = cm.display; + if (op.updatedDisplay) updateHeightsInViewport(cm); + + op.barMeasure = measureForScrollbars(cm); + + // If the max line changed since it was last measured, measure it, + // and ensure the document's width matches it. + // updateDisplay_W2 will use these properties to do the actual resizing + if (display.maxLineChanged && !cm.options.lineWrapping) { + op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3; + cm.display.sizerWidth = op.adjustWidthTo; + op.barMeasure.scrollWidth = + Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth); + op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm)); + } + + if (op.updatedDisplay || op.selectionChanged) + op.preparedSelection = display.input.prepareSelection(op.focus); + } + + function endOperation_W2(op) { + var cm = op.cm; + + if (op.adjustWidthTo != null) { + cm.display.sizer.style.minWidth = op.adjustWidthTo + "px"; + if (op.maxScrollLeft < cm.doc.scrollLeft) + setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true); + cm.display.maxLineChanged = false; + } + + var takeFocus = op.focus && op.focus == activeElt() && (!document.hasFocus || document.hasFocus()) + if (op.preparedSelection) + cm.display.input.showSelection(op.preparedSelection, takeFocus); + if (op.updatedDisplay || op.startHeight != cm.doc.height) + updateScrollbars(cm, op.barMeasure); + if (op.updatedDisplay) + setDocumentHeight(cm, op.barMeasure); + + if (op.selectionChanged) restartBlink(cm); + + if (cm.state.focused && op.updateInput) + cm.display.input.reset(op.typing); + if (takeFocus) ensureFocus(op.cm); + } + + function endOperation_finish(op) { + var cm = op.cm, display = cm.display, doc = cm.doc; + + if (op.updatedDisplay) postUpdateDisplay(cm, op.update); + + // Abort mouse wheel delta measurement, when scrolling explicitly + if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos)) + display.wheelStartX = display.wheelStartY = null; + + // Propagate the scroll position to the actual DOM scroller + if (op.scrollTop != null && (display.scroller.scrollTop != op.scrollTop || op.forceScroll)) { + doc.scrollTop = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, op.scrollTop)); + display.scrollbars.setScrollTop(doc.scrollTop); + display.scroller.scrollTop = doc.scrollTop; + } + if (op.scrollLeft != null && (display.scroller.scrollLeft != op.scrollLeft || op.forceScroll)) { + doc.scrollLeft = Math.max(0, Math.min(display.scroller.scrollWidth - display.scroller.clientWidth, op.scrollLeft)); + display.scrollbars.setScrollLeft(doc.scrollLeft); + display.scroller.scrollLeft = doc.scrollLeft; + alignHorizontally(cm); + } + // If we need to scroll a specific position into view, do so. + if (op.scrollToPos) { + var coords = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from), + clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin); + if (op.scrollToPos.isCursor && cm.state.focused) maybeScrollWindow(cm, coords); + } + + // Fire events for markers that are hidden/unidden by editing or + // undoing + var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers; + if (hidden) for (var i = 0; i < hidden.length; ++i) + if (!hidden[i].lines.length) signal(hidden[i], "hide"); + if (unhidden) for (var i = 0; i < unhidden.length; ++i) + if (unhidden[i].lines.length) signal(unhidden[i], "unhide"); + + if (display.wrapper.offsetHeight) + doc.scrollTop = cm.display.scroller.scrollTop; + + // Fire change events, and delayed event handlers + if (op.changeObjs) + signal(cm, "changes", cm, op.changeObjs); + if (op.update) + op.update.finish(); + } + + // Run the given function in an operation + function runInOp(cm, f) { + if (cm.curOp) return f(); + startOperation(cm); + try { return f(); } + finally { endOperation(cm); } + } + // Wraps a function in an operation. Returns the wrapped function. + function operation(cm, f) { + return function() { + if (cm.curOp) return f.apply(cm, arguments); + startOperation(cm); + try { return f.apply(cm, arguments); } + finally { endOperation(cm); } + }; + } + // Used to add methods to editor and doc instances, wrapping them in + // operations. + function methodOp(f) { + return function() { + if (this.curOp) return f.apply(this, arguments); + startOperation(this); + try { return f.apply(this, arguments); } + finally { endOperation(this); } + }; + } + function docMethodOp(f) { + return function() { + var cm = this.cm; + if (!cm || cm.curOp) return f.apply(this, arguments); + startOperation(cm); + try { return f.apply(this, arguments); } + finally { endOperation(cm); } + }; + } + + // VIEW TRACKING + + // These objects are used to represent the visible (currently drawn) + // part of the document. A LineView may correspond to multiple + // logical lines, if those are connected by collapsed ranges. + function LineView(doc, line, lineN) { + // The starting line + this.line = line; + // Continuing lines, if any + this.rest = visualLineContinued(line); + // Number of logical lines in this visual line + this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1; + this.node = this.text = null; + this.hidden = lineIsHidden(doc, line); + } + + // Create a range of LineView objects for the given lines. + function buildViewArray(cm, from, to) { + var array = [], nextPos; + for (var pos = from; pos < to; pos = nextPos) { + var view = new LineView(cm.doc, getLine(cm.doc, pos), pos); + nextPos = pos + view.size; + array.push(view); + } + return array; + } + + // Updates the display.view data structure for a given change to the + // document. From and to are in pre-change coordinates. Lendiff is + // the amount of lines added or subtracted by the change. This is + // used for changes that span multiple lines, or change the way + // lines are divided into visual lines. regLineChange (below) + // registers single-line changes. + function regChange(cm, from, to, lendiff) { + if (from == null) from = cm.doc.first; + if (to == null) to = cm.doc.first + cm.doc.size; + if (!lendiff) lendiff = 0; + + var display = cm.display; + if (lendiff && to < display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers > from)) + display.updateLineNumbers = from; + + cm.curOp.viewChanged = true; + + if (from >= display.viewTo) { // Change after + if (sawCollapsedSpans && visualLineNo(cm.doc, from) < display.viewTo) + resetView(cm); + } else if (to <= display.viewFrom) { // Change before + if (sawCollapsedSpans && visualLineEndNo(cm.doc, to + lendiff) > display.viewFrom) { + resetView(cm); + } else { + display.viewFrom += lendiff; + display.viewTo += lendiff; + } + } else if (from <= display.viewFrom && to >= display.viewTo) { // Full overlap + resetView(cm); + } else if (from <= display.viewFrom) { // Top overlap + var cut = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cut) { + display.view = display.view.slice(cut.index); + display.viewFrom = cut.lineN; + display.viewTo += lendiff; + } else { + resetView(cm); + } + } else if (to >= display.viewTo) { // Bottom overlap + var cut = viewCuttingPoint(cm, from, from, -1); + if (cut) { + display.view = display.view.slice(0, cut.index); + display.viewTo = cut.lineN; + } else { + resetView(cm); + } + } else { // Gap in the middle + var cutTop = viewCuttingPoint(cm, from, from, -1); + var cutBot = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cutTop && cutBot) { + display.view = display.view.slice(0, cutTop.index) + .concat(buildViewArray(cm, cutTop.lineN, cutBot.lineN)) + .concat(display.view.slice(cutBot.index)); + display.viewTo += lendiff; + } else { + resetView(cm); + } + } + + var ext = display.externalMeasured; + if (ext) { + if (to < ext.lineN) + ext.lineN += lendiff; + else if (from < ext.lineN + ext.size) + display.externalMeasured = null; + } + } + + // Register a change to a single line. Type must be one of "text", + // "gutter", "class", "widget" + function regLineChange(cm, line, type) { + cm.curOp.viewChanged = true; + var display = cm.display, ext = cm.display.externalMeasured; + if (ext && line >= ext.lineN && line < ext.lineN + ext.size) + display.externalMeasured = null; + + if (line < display.viewFrom || line >= display.viewTo) return; + var lineView = display.view[findViewIndex(cm, line)]; + if (lineView.node == null) return; + var arr = lineView.changes || (lineView.changes = []); + if (indexOf(arr, type) == -1) arr.push(type); + } + + // Clear the view. + function resetView(cm) { + cm.display.viewFrom = cm.display.viewTo = cm.doc.first; + cm.display.view = []; + cm.display.viewOffset = 0; + } + + // Find the view element corresponding to a given line. Return null + // when the line isn't visible. + function findViewIndex(cm, n) { + if (n >= cm.display.viewTo) return null; + n -= cm.display.viewFrom; + if (n < 0) return null; + var view = cm.display.view; + for (var i = 0; i < view.length; i++) { + n -= view[i].size; + if (n < 0) return i; + } + } + + function viewCuttingPoint(cm, oldN, newN, dir) { + var index = findViewIndex(cm, oldN), diff, view = cm.display.view; + if (!sawCollapsedSpans || newN == cm.doc.first + cm.doc.size) + return {index: index, lineN: newN}; + for (var i = 0, n = cm.display.viewFrom; i < index; i++) + n += view[i].size; + if (n != oldN) { + if (dir > 0) { + if (index == view.length - 1) return null; + diff = (n + view[index].size) - oldN; + index++; + } else { + diff = n - oldN; + } + oldN += diff; newN += diff; + } + while (visualLineNo(cm.doc, newN) != newN) { + if (index == (dir < 0 ? 0 : view.length - 1)) return null; + newN += dir * view[index - (dir < 0 ? 1 : 0)].size; + index += dir; + } + return {index: index, lineN: newN}; + } + + // Force the view to cover a given range, adding empty view element + // or clipping off existing ones as needed. + function adjustView(cm, from, to) { + var display = cm.display, view = display.view; + if (view.length == 0 || from >= display.viewTo || to <= display.viewFrom) { + display.view = buildViewArray(cm, from, to); + display.viewFrom = from; + } else { + if (display.viewFrom > from) + display.view = buildViewArray(cm, from, display.viewFrom).concat(display.view); + else if (display.viewFrom < from) + display.view = display.view.slice(findViewIndex(cm, from)); + display.viewFrom = from; + if (display.viewTo < to) + display.view = display.view.concat(buildViewArray(cm, display.viewTo, to)); + else if (display.viewTo > to) + display.view = display.view.slice(0, findViewIndex(cm, to)); + } + display.viewTo = to; + } + + // Count the number of lines in the view whose DOM representation is + // out of date (or nonexistent). + function countDirtyView(cm) { + var view = cm.display.view, dirty = 0; + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (!lineView.hidden && (!lineView.node || lineView.changes)) ++dirty; + } + return dirty; + } + + // EVENT HANDLERS + + // Attach the necessary event handlers when initializing the editor + function registerEventHandlers(cm) { + var d = cm.display; + on(d.scroller, "mousedown", operation(cm, onMouseDown)); + // Older IE's will not fire a second mousedown for a double click + if (ie && ie_version < 11) + on(d.scroller, "dblclick", operation(cm, function(e) { + if (signalDOMEvent(cm, e)) return; + var pos = posFromMouse(cm, e); + if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) return; + e_preventDefault(e); + var word = cm.findWordAt(pos); + extendSelection(cm.doc, word.anchor, word.head); + })); + else + on(d.scroller, "dblclick", function(e) { signalDOMEvent(cm, e) || e_preventDefault(e); }); + // Some browsers fire contextmenu *after* opening the menu, at + // which point we can't mess with it anymore. Context menu is + // handled in onMouseDown for these browsers. + if (!captureRightClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);}); + + // Used to suppress mouse event handling when a touch happens + var touchFinished, prevTouch = {end: 0}; + function finishTouch() { + if (d.activeTouch) { + touchFinished = setTimeout(function() {d.activeTouch = null;}, 1000); + prevTouch = d.activeTouch; + prevTouch.end = +new Date; + } + }; + function isMouseLikeTouchEvent(e) { + if (e.touches.length != 1) return false; + var touch = e.touches[0]; + return touch.radiusX <= 1 && touch.radiusY <= 1; + } + function farAway(touch, other) { + if (other.left == null) return true; + var dx = other.left - touch.left, dy = other.top - touch.top; + return dx * dx + dy * dy > 20 * 20; + } + on(d.scroller, "touchstart", function(e) { + if (!signalDOMEvent(cm, e) && !isMouseLikeTouchEvent(e)) { + clearTimeout(touchFinished); + var now = +new Date; + d.activeTouch = {start: now, moved: false, + prev: now - prevTouch.end <= 300 ? prevTouch : null}; + if (e.touches.length == 1) { + d.activeTouch.left = e.touches[0].pageX; + d.activeTouch.top = e.touches[0].pageY; + } + } + }); + on(d.scroller, "touchmove", function() { + if (d.activeTouch) d.activeTouch.moved = true; + }); + on(d.scroller, "touchend", function(e) { + var touch = d.activeTouch; + if (touch && !eventInWidget(d, e) && touch.left != null && + !touch.moved && new Date - touch.start < 300) { + var pos = cm.coordsChar(d.activeTouch, "page"), range; + if (!touch.prev || farAway(touch, touch.prev)) // Single tap + range = new Range(pos, pos); + else if (!touch.prev.prev || farAway(touch, touch.prev.prev)) // Double tap + range = cm.findWordAt(pos); + else // Triple tap + range = new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))); + cm.setSelection(range.anchor, range.head); + cm.focus(); + e_preventDefault(e); + } + finishTouch(); + }); + on(d.scroller, "touchcancel", finishTouch); + + // Sync scrolling between fake scrollbars and real scrollable + // area, ensure viewport is updated when scrolling. + on(d.scroller, "scroll", function() { + if (d.scroller.clientHeight) { + setScrollTop(cm, d.scroller.scrollTop); + setScrollLeft(cm, d.scroller.scrollLeft, true); + signal(cm, "scroll", cm); + } + }); + + // Listen to wheel events in order to try and update the viewport on time. + on(d.scroller, "mousewheel", function(e){onScrollWheel(cm, e);}); + on(d.scroller, "DOMMouseScroll", function(e){onScrollWheel(cm, e);}); + + // Prevent wrapper from ever scrolling + on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }); + + d.dragFunctions = { + enter: function(e) {if (!signalDOMEvent(cm, e)) e_stop(e);}, + over: function(e) {if (!signalDOMEvent(cm, e)) { onDragOver(cm, e); e_stop(e); }}, + start: function(e){onDragStart(cm, e);}, + drop: operation(cm, onDrop), + leave: function(e) {if (!signalDOMEvent(cm, e)) { clearDragCursor(cm); }} + }; + + var inp = d.input.getField(); + on(inp, "keyup", function(e) { onKeyUp.call(cm, e); }); + on(inp, "keydown", operation(cm, onKeyDown)); + on(inp, "keypress", operation(cm, onKeyPress)); + on(inp, "focus", bind(onFocus, cm)); + on(inp, "blur", bind(onBlur, cm)); + } + + function dragDropChanged(cm, value, old) { + var wasOn = old && old != CodeMirror.Init; + if (!value != !wasOn) { + var funcs = cm.display.dragFunctions; + var toggle = value ? on : off; + toggle(cm.display.scroller, "dragstart", funcs.start); + toggle(cm.display.scroller, "dragenter", funcs.enter); + toggle(cm.display.scroller, "dragover", funcs.over); + toggle(cm.display.scroller, "dragleave", funcs.leave); + toggle(cm.display.scroller, "drop", funcs.drop); + } + } + + // Called when the window resizes + function onResize(cm) { + var d = cm.display; + if (d.lastWrapHeight == d.wrapper.clientHeight && d.lastWrapWidth == d.wrapper.clientWidth) + return; + // Might be a text scaling operation, clear size caches. + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; + d.scrollbarsClipped = false; + cm.setSize(); + } + + // MOUSE EVENTS + + // Return true when the given mouse event happened in a widget + function eventInWidget(display, e) { + for (var n = e_target(e); n != display.wrapper; n = n.parentNode) { + if (!n || (n.nodeType == 1 && n.getAttribute("cm-ignore-events") == "true") || + (n.parentNode == display.sizer && n != display.mover)) + return true; + } + } + + // Given a mouse event, find the corresponding position. If liberal + // is false, it checks whether a gutter or scrollbar was clicked, + // and returns null if it was. forRect is used by rectangular + // selections, and tries to estimate a character position even for + // coordinates beyond the right of the text. + function posFromMouse(cm, e, liberal, forRect) { + var display = cm.display; + if (!liberal && e_target(e).getAttribute("cm-not-content") == "true") return null; + + var x, y, space = display.lineSpace.getBoundingClientRect(); + // Fails unpredictably on IE[67] when mouse is dragged around quickly. + try { x = e.clientX - space.left; y = e.clientY - space.top; } + catch (e) { return null; } + var coords = coordsChar(cm, x, y), line; + if (forRect && coords.xRel == 1 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) { + var colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length; + coords = Pos(coords.line, Math.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff)); + } + return coords; + } + + // A mouse down can be a single click, double click, triple click, + // start of selection drag, start of text drag, new cursor + // (ctrl-click), rectangle drag (alt-drag), or xwin + // middle-click-paste. Or it might be a click on something we should + // not interfere with, such as a scrollbar or widget. + function onMouseDown(e) { + var cm = this, display = cm.display; + if (signalDOMEvent(cm, e) || display.activeTouch && display.input.supportsTouch()) return; + display.shift = e.shiftKey; + + if (eventInWidget(display, e)) { + if (!webkit) { + // Briefly turn off draggability, to allow widgets to do + // normal dragging things. + display.scroller.draggable = false; + setTimeout(function(){display.scroller.draggable = true;}, 100); + } + return; + } + if (clickInGutter(cm, e)) return; + var start = posFromMouse(cm, e); + window.focus(); + + switch (e_button(e)) { + case 1: + // #3261: make sure, that we're not starting a second selection + if (cm.state.selectingText) + cm.state.selectingText(e); + else if (start) + leftButtonDown(cm, e, start); + else if (e_target(e) == display.scroller) + e_preventDefault(e); + break; + case 2: + if (webkit) cm.state.lastMiddleDown = +new Date; + if (start) extendSelection(cm.doc, start); + setTimeout(function() {display.input.focus();}, 20); + e_preventDefault(e); + break; + case 3: + if (captureRightClick) onContextMenu(cm, e); + else delayBlurEvent(cm); + break; + } + } + + var lastClick, lastDoubleClick; + function leftButtonDown(cm, e, start) { + if (ie) setTimeout(bind(ensureFocus, cm), 0); + else cm.curOp.focus = activeElt(); + + var now = +new Date, type; + if (lastDoubleClick && lastDoubleClick.time > now - 400 && cmp(lastDoubleClick.pos, start) == 0) { + type = "triple"; + } else if (lastClick && lastClick.time > now - 400 && cmp(lastClick.pos, start) == 0) { + type = "double"; + lastDoubleClick = {time: now, pos: start}; + } else { + type = "single"; + lastClick = {time: now, pos: start}; + } + + var sel = cm.doc.sel, modifier = mac ? e.metaKey : e.ctrlKey, contained; + if (cm.options.dragDrop && dragAndDrop && !cm.isReadOnly() && + type == "single" && (contained = sel.contains(start)) > -1 && + (cmp((contained = sel.ranges[contained]).from(), start) < 0 || start.xRel > 0) && + (cmp(contained.to(), start) > 0 || start.xRel < 0)) + leftButtonStartDrag(cm, e, start, modifier); + else + leftButtonSelect(cm, e, start, type, modifier); + } + + // Start a text drag. When it ends, see if any dragging actually + // happen, and treat as a click if it didn't. + function leftButtonStartDrag(cm, e, start, modifier) { + var display = cm.display, startTime = +new Date; + var dragEnd = operation(cm, function(e2) { + if (webkit) display.scroller.draggable = false; + cm.state.draggingText = false; + off(document, "mouseup", dragEnd); + off(display.scroller, "drop", dragEnd); + if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) { + e_preventDefault(e2); + if (!modifier && +new Date - 200 < startTime) + extendSelection(cm.doc, start); + // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081) + if (webkit || ie && ie_version == 9) + setTimeout(function() {document.body.focus(); display.input.focus();}, 20); + else + display.input.focus(); + } + }); + // Let the drag handler handle this. + if (webkit) display.scroller.draggable = true; + cm.state.draggingText = dragEnd; + dragEnd.copy = mac ? e.altKey : e.ctrlKey + // IE's approach to draggable + if (display.scroller.dragDrop) display.scroller.dragDrop(); + on(document, "mouseup", dragEnd); + on(display.scroller, "drop", dragEnd); + } + + // Normal selection, as opposed to text dragging. + function leftButtonSelect(cm, e, start, type, addNew) { + var display = cm.display, doc = cm.doc; + e_preventDefault(e); + + var ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges; + if (addNew && !e.shiftKey) { + ourIndex = doc.sel.contains(start); + if (ourIndex > -1) + ourRange = ranges[ourIndex]; + else + ourRange = new Range(start, start); + } else { + ourRange = doc.sel.primary(); + ourIndex = doc.sel.primIndex; + } + + if (chromeOS ? e.shiftKey && e.metaKey : e.altKey) { + type = "rect"; + if (!addNew) ourRange = new Range(start, start); + start = posFromMouse(cm, e, true, true); + ourIndex = -1; + } else if (type == "double") { + var word = cm.findWordAt(start); + if (cm.display.shift || doc.extend) + ourRange = extendRange(doc, ourRange, word.anchor, word.head); + else + ourRange = word; + } else if (type == "triple") { + var line = new Range(Pos(start.line, 0), clipPos(doc, Pos(start.line + 1, 0))); + if (cm.display.shift || doc.extend) + ourRange = extendRange(doc, ourRange, line.anchor, line.head); + else + ourRange = line; + } else { + ourRange = extendRange(doc, ourRange, start); + } + + if (!addNew) { + ourIndex = 0; + setSelection(doc, new Selection([ourRange], 0), sel_mouse); + startSel = doc.sel; + } else if (ourIndex == -1) { + ourIndex = ranges.length; + setSelection(doc, normalizeSelection(ranges.concat([ourRange]), ourIndex), + {scroll: false, origin: "*mouse"}); + } else if (ranges.length > 1 && ranges[ourIndex].empty() && type == "single" && !e.shiftKey) { + setSelection(doc, normalizeSelection(ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0), + {scroll: false, origin: "*mouse"}); + startSel = doc.sel; + } else { + replaceOneSelection(doc, ourIndex, ourRange, sel_mouse); + } + + var lastPos = start; + function extendTo(pos) { + if (cmp(lastPos, pos) == 0) return; + lastPos = pos; + + if (type == "rect") { + var ranges = [], tabSize = cm.options.tabSize; + var startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize); + var posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize); + var left = Math.min(startCol, posCol), right = Math.max(startCol, posCol); + for (var line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line)); + line <= end; line++) { + var text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize); + if (left == right) + ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos))); + else if (text.length > leftPos) + ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize)))); + } + if (!ranges.length) ranges.push(new Range(start, start)); + setSelection(doc, normalizeSelection(startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex), + {origin: "*mouse", scroll: false}); + cm.scrollIntoView(pos); + } else { + var oldRange = ourRange; + var anchor = oldRange.anchor, head = pos; + if (type != "single") { + if (type == "double") + var range = cm.findWordAt(pos); + else + var range = new Range(Pos(pos.line, 0), clipPos(doc, Pos(pos.line + 1, 0))); + if (cmp(range.anchor, anchor) > 0) { + head = range.head; + anchor = minPos(oldRange.from(), range.anchor); + } else { + head = range.anchor; + anchor = maxPos(oldRange.to(), range.head); + } + } + var ranges = startSel.ranges.slice(0); + ranges[ourIndex] = new Range(clipPos(doc, anchor), head); + setSelection(doc, normalizeSelection(ranges, ourIndex), sel_mouse); + } + } + + var editorSize = display.wrapper.getBoundingClientRect(); + // Used to ensure timeout re-tries don't fire when another extend + // happened in the meantime (clearTimeout isn't reliable -- at + // least on Chrome, the timeouts still happen even when cleared, + // if the clear happens after their scheduled firing time). + var counter = 0; + + function extend(e) { + var curCount = ++counter; + var cur = posFromMouse(cm, e, true, type == "rect"); + if (!cur) return; + if (cmp(cur, lastPos) != 0) { + cm.curOp.focus = activeElt(); + extendTo(cur); + var visible = visibleLines(display, doc); + if (cur.line >= visible.to || cur.line < visible.from) + setTimeout(operation(cm, function(){if (counter == curCount) extend(e);}), 150); + } else { + var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0; + if (outside) setTimeout(operation(cm, function() { + if (counter != curCount) return; + display.scroller.scrollTop += outside; + extend(e); + }), 50); + } + } + + function done(e) { + cm.state.selectingText = false; + counter = Infinity; + e_preventDefault(e); + display.input.focus(); + off(document, "mousemove", move); + off(document, "mouseup", up); + doc.history.lastSelOrigin = null; + } + + var move = operation(cm, function(e) { + if (!e_button(e)) done(e); + else extend(e); + }); + var up = operation(cm, done); + cm.state.selectingText = up; + on(document, "mousemove", move); + on(document, "mouseup", up); + } + + // Determines whether an event happened in the gutter, and fires the + // handlers for the corresponding event. + function gutterEvent(cm, e, type, prevent) { + try { var mX = e.clientX, mY = e.clientY; } + catch(e) { return false; } + if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) return false; + if (prevent) e_preventDefault(e); + + var display = cm.display; + var lineBox = display.lineDiv.getBoundingClientRect(); + + if (mY > lineBox.bottom || !hasHandler(cm, type)) return e_defaultPrevented(e); + mY -= lineBox.top - display.viewOffset; + + for (var i = 0; i < cm.options.gutters.length; ++i) { + var g = display.gutters.childNodes[i]; + if (g && g.getBoundingClientRect().right >= mX) { + var line = lineAtHeight(cm.doc, mY); + var gutter = cm.options.gutters[i]; + signal(cm, type, cm, line, gutter, e); + return e_defaultPrevented(e); + } + } + } + + function clickInGutter(cm, e) { + return gutterEvent(cm, e, "gutterClick", true); + } + + // Kludge to work around strange IE behavior where it'll sometimes + // re-fire a series of drag-related events right after the drop (#1551) + var lastDrop = 0; + + function onDrop(e) { + var cm = this; + clearDragCursor(cm); + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) + return; + e_preventDefault(e); + if (ie) lastDrop = +new Date; + var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files; + if (!pos || cm.isReadOnly()) return; + // Might be a file drop, in which case we simply extract the text + // and insert it. + if (files && files.length && window.FileReader && window.File) { + var n = files.length, text = Array(n), read = 0; + var loadFile = function(file, i) { + if (cm.options.allowDropFileTypes && + indexOf(cm.options.allowDropFileTypes, file.type) == -1) + return; + + var reader = new FileReader; + reader.onload = operation(cm, function() { + var content = reader.result; + if (/[\x00-\x08\x0e-\x1f]{2}/.test(content)) content = ""; + text[i] = content; + if (++read == n) { + pos = clipPos(cm.doc, pos); + var change = {from: pos, to: pos, + text: cm.doc.splitLines(text.join(cm.doc.lineSeparator())), + origin: "paste"}; + makeChange(cm.doc, change); + setSelectionReplaceHistory(cm.doc, simpleSelection(pos, changeEnd(change))); + } + }); + reader.readAsText(file); + }; + for (var i = 0; i < n; ++i) loadFile(files[i], i); + } else { // Normal drop + // Don't do a replace if the drop happened inside of the selected text. + if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) { + cm.state.draggingText(e); + // Ensure the editor is re-focused + setTimeout(function() {cm.display.input.focus();}, 20); + return; + } + try { + var text = e.dataTransfer.getData("Text"); + if (text) { + if (cm.state.draggingText && !cm.state.draggingText.copy) + var selected = cm.listSelections(); + setSelectionNoUndo(cm.doc, simpleSelection(pos, pos)); + if (selected) for (var i = 0; i < selected.length; ++i) + replaceRange(cm.doc, "", selected[i].anchor, selected[i].head, "drag"); + cm.replaceSelection(text, "around", "paste"); + cm.display.input.focus(); + } + } + catch(e){} + } + } + + function onDragStart(cm, e) { + if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return; } + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return; + + e.dataTransfer.setData("Text", cm.getSelection()); + e.dataTransfer.effectAllowed = "copyMove" + + // Use dummy image instead of default browsers image. + // Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there. + if (e.dataTransfer.setDragImage && !safari) { + var img = elt("img", null, null, "position: fixed; left: 0; top: 0;"); + img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; + if (presto) { + img.width = img.height = 1; + cm.display.wrapper.appendChild(img); + // Force a relayout, or Opera won't use our image for some obscure reason + img._top = img.offsetTop; + } + e.dataTransfer.setDragImage(img, 0, 0); + if (presto) img.parentNode.removeChild(img); + } + } + + function onDragOver(cm, e) { + var pos = posFromMouse(cm, e); + if (!pos) return; + var frag = document.createDocumentFragment(); + drawSelectionCursor(cm, pos, frag); + if (!cm.display.dragCursor) { + cm.display.dragCursor = elt("div", null, "CodeMirror-cursors CodeMirror-dragcursors"); + cm.display.lineSpace.insertBefore(cm.display.dragCursor, cm.display.cursorDiv); + } + removeChildrenAndAdd(cm.display.dragCursor, frag); + } + + function clearDragCursor(cm) { + if (cm.display.dragCursor) { + cm.display.lineSpace.removeChild(cm.display.dragCursor); + cm.display.dragCursor = null; + } + } + + // SCROLL EVENTS + + // Sync the scrollable area and scrollbars, ensure the viewport + // covers the visible area. + function setScrollTop(cm, val) { + if (Math.abs(cm.doc.scrollTop - val) < 2) return; + cm.doc.scrollTop = val; + if (!gecko) updateDisplaySimple(cm, {top: val}); + if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val; + cm.display.scrollbars.setScrollTop(val); + if (gecko) updateDisplaySimple(cm); + startWorker(cm, 100); + } + // Sync scroller and scrollbar, ensure the gutter elements are + // aligned. + function setScrollLeft(cm, val, isScroller) { + if (isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) return; + val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth); + cm.doc.scrollLeft = val; + alignHorizontally(cm); + if (cm.display.scroller.scrollLeft != val) cm.display.scroller.scrollLeft = val; + cm.display.scrollbars.setScrollLeft(val); + } + + // Since the delta values reported on mouse wheel events are + // unstandardized between browsers and even browser versions, and + // generally horribly unpredictable, this code starts by measuring + // the scroll effect that the first few mouse wheel events have, + // and, from that, detects the way it can convert deltas to pixel + // offsets afterwards. + // + // The reason we want to know the amount a wheel event will scroll + // is that it gives us a chance to update the display before the + // actual scrolling happens, reducing flickering. + + var wheelSamples = 0, wheelPixelsPerUnit = null; + // Fill in a browser-detected starting value on browsers where we + // know one. These don't have to be accurate -- the result of them + // being wrong would just be a slight flicker on the first wheel + // scroll (if it is large enough). + if (ie) wheelPixelsPerUnit = -.53; + else if (gecko) wheelPixelsPerUnit = 15; + else if (chrome) wheelPixelsPerUnit = -.7; + else if (safari) wheelPixelsPerUnit = -1/3; + + var wheelEventDelta = function(e) { + var dx = e.wheelDeltaX, dy = e.wheelDeltaY; + if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) dx = e.detail; + if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) dy = e.detail; + else if (dy == null) dy = e.wheelDelta; + return {x: dx, y: dy}; + }; + CodeMirror.wheelEventPixels = function(e) { + var delta = wheelEventDelta(e); + delta.x *= wheelPixelsPerUnit; + delta.y *= wheelPixelsPerUnit; + return delta; + }; + + function onScrollWheel(cm, e) { + var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y; + + var display = cm.display, scroll = display.scroller; + // Quit if there's nothing to scroll here + var canScrollX = scroll.scrollWidth > scroll.clientWidth; + var canScrollY = scroll.scrollHeight > scroll.clientHeight; + if (!(dx && canScrollX || dy && canScrollY)) return; + + // Webkit browsers on OS X abort momentum scrolls when the target + // of the scroll event is removed from the scrollable element. + // This hack (see related code in patchDisplay) makes sure the + // element is kept around. + if (dy && mac && webkit) { + outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) { + for (var i = 0; i < view.length; i++) { + if (view[i].node == cur) { + cm.display.currentWheelTarget = cur; + break outer; + } + } + } + } + + // On some browsers, horizontal scrolling will cause redraws to + // happen before the gutter has been realigned, causing it to + // wriggle around in a most unseemly way. When we have an + // estimated pixels/delta value, we just handle horizontal + // scrolling entirely here. It'll be slightly off from native, but + // better than glitching out. + if (dx && !gecko && !presto && wheelPixelsPerUnit != null) { + if (dy && canScrollY) + setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight))); + setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth))); + // Only prevent default scrolling if vertical scrolling is + // actually possible. Otherwise, it causes vertical scroll + // jitter on OSX trackpads when deltaX is small and deltaY + // is large (issue #3579) + if (!dy || (dy && canScrollY)) + e_preventDefault(e); + display.wheelStartX = null; // Abort measurement, if in progress + return; + } + + // 'Project' the visible viewport to cover the area that is being + // scrolled into view (if we know enough to estimate it). + if (dy && wheelPixelsPerUnit != null) { + var pixels = dy * wheelPixelsPerUnit; + var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight; + if (pixels < 0) top = Math.max(0, top + pixels - 50); + else bot = Math.min(cm.doc.height, bot + pixels + 50); + updateDisplaySimple(cm, {top: top, bottom: bot}); + } + + if (wheelSamples < 20) { + if (display.wheelStartX == null) { + display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop; + display.wheelDX = dx; display.wheelDY = dy; + setTimeout(function() { + if (display.wheelStartX == null) return; + var movedX = scroll.scrollLeft - display.wheelStartX; + var movedY = scroll.scrollTop - display.wheelStartY; + var sample = (movedY && display.wheelDY && movedY / display.wheelDY) || + (movedX && display.wheelDX && movedX / display.wheelDX); + display.wheelStartX = display.wheelStartY = null; + if (!sample) return; + wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1); + ++wheelSamples; + }, 200); + } else { + display.wheelDX += dx; display.wheelDY += dy; + } + } + } + + // KEY EVENTS + + // Run a handler that was bound to a key. + function doHandleBinding(cm, bound, dropShift) { + if (typeof bound == "string") { + bound = commands[bound]; + if (!bound) return false; + } + // Ensure previous input has been read, so that the handler sees a + // consistent view of the document + cm.display.input.ensurePolled(); + var prevShift = cm.display.shift, done = false; + try { + if (cm.isReadOnly()) cm.state.suppressEdits = true; + if (dropShift) cm.display.shift = false; + done = bound(cm) != Pass; + } finally { + cm.display.shift = prevShift; + cm.state.suppressEdits = false; + } + return done; + } + + function lookupKeyForEditor(cm, name, handle) { + for (var i = 0; i < cm.state.keyMaps.length; i++) { + var result = lookupKey(name, cm.state.keyMaps[i], handle, cm); + if (result) return result; + } + return (cm.options.extraKeys && lookupKey(name, cm.options.extraKeys, handle, cm)) + || lookupKey(name, cm.options.keyMap, handle, cm); + } + + var stopSeq = new Delayed; + function dispatchKey(cm, name, e, handle) { + var seq = cm.state.keySeq; + if (seq) { + if (isModifierKey(name)) return "handled"; + stopSeq.set(50, function() { + if (cm.state.keySeq == seq) { + cm.state.keySeq = null; + cm.display.input.reset(); + } + }); + name = seq + " " + name; + } + var result = lookupKeyForEditor(cm, name, handle); + + if (result == "multi") + cm.state.keySeq = name; + if (result == "handled") + signalLater(cm, "keyHandled", cm, name, e); + + if (result == "handled" || result == "multi") { + e_preventDefault(e); + restartBlink(cm); + } + + if (seq && !result && /\'$/.test(name)) { + e_preventDefault(e); + return true; + } + return !!result; + } + + // Handle a key from the keydown event. + function handleKeyBinding(cm, e) { + var name = keyName(e, true); + if (!name) return false; + + if (e.shiftKey && !cm.state.keySeq) { + // First try to resolve full name (including 'Shift-'). Failing + // that, see if there is a cursor-motion command (starting with + // 'go') bound to the keyname without 'Shift-'. + return dispatchKey(cm, "Shift-" + name, e, function(b) {return doHandleBinding(cm, b, true);}) + || dispatchKey(cm, name, e, function(b) { + if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion) + return doHandleBinding(cm, b); + }); + } else { + return dispatchKey(cm, name, e, function(b) { return doHandleBinding(cm, b); }); + } + } + + // Handle a key from the keypress event + function handleCharBinding(cm, e, ch) { + return dispatchKey(cm, "'" + ch + "'", e, + function(b) { return doHandleBinding(cm, b, true); }); + } + + var lastStoppedKey = null; + function onKeyDown(e) { + var cm = this; + cm.curOp.focus = activeElt(); + if (signalDOMEvent(cm, e)) return; + // IE does strange things with escape. + if (ie && ie_version < 11 && e.keyCode == 27) e.returnValue = false; + var code = e.keyCode; + cm.display.shift = code == 16 || e.shiftKey; + var handled = handleKeyBinding(cm, e); + if (presto) { + lastStoppedKey = handled ? code : null; + // Opera has no cut event... we try to at least catch the key combo + if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey)) + cm.replaceSelection("", null, "cut"); + } + + // Turn mouse into crosshair when Alt is held on Mac. + if (code == 18 && !/\bCodeMirror-crosshair\b/.test(cm.display.lineDiv.className)) + showCrossHair(cm); + } + + function showCrossHair(cm) { + var lineDiv = cm.display.lineDiv; + addClass(lineDiv, "CodeMirror-crosshair"); + + function up(e) { + if (e.keyCode == 18 || !e.altKey) { + rmClass(lineDiv, "CodeMirror-crosshair"); + off(document, "keyup", up); + off(document, "mouseover", up); + } + } + on(document, "keyup", up); + on(document, "mouseover", up); + } + + function onKeyUp(e) { + if (e.keyCode == 16) this.doc.sel.shift = false; + signalDOMEvent(this, e); + } + + function onKeyPress(e) { + var cm = this; + if (eventInWidget(cm.display, e) || signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) return; + var keyCode = e.keyCode, charCode = e.charCode; + if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;} + if ((presto && (!e.which || e.which < 10)) && handleKeyBinding(cm, e)) return; + var ch = String.fromCharCode(charCode == null ? keyCode : charCode); + if (handleCharBinding(cm, e, ch)) return; + cm.display.input.onKeyPress(e); + } + + // FOCUS/BLUR EVENTS + + function delayBlurEvent(cm) { + cm.state.delayingBlurEvent = true; + setTimeout(function() { + if (cm.state.delayingBlurEvent) { + cm.state.delayingBlurEvent = false; + onBlur(cm); + } + }, 100); + } + + function onFocus(cm) { + if (cm.state.delayingBlurEvent) cm.state.delayingBlurEvent = false; + + if (cm.options.readOnly == "nocursor") return; + if (!cm.state.focused) { + signal(cm, "focus", cm); + cm.state.focused = true; + addClass(cm.display.wrapper, "CodeMirror-focused"); + // This test prevents this from firing when a context + // menu is closed (since the input reset would kill the + // select-all detection hack) + if (!cm.curOp && cm.display.selForContextMenu != cm.doc.sel) { + cm.display.input.reset(); + if (webkit) setTimeout(function() { cm.display.input.reset(true); }, 20); // Issue #1730 + } + cm.display.input.receivedFocus(); + } + restartBlink(cm); + } + function onBlur(cm) { + if (cm.state.delayingBlurEvent) return; + + if (cm.state.focused) { + signal(cm, "blur", cm); + cm.state.focused = false; + rmClass(cm.display.wrapper, "CodeMirror-focused"); + } + clearInterval(cm.display.blinker); + setTimeout(function() {if (!cm.state.focused) cm.display.shift = false;}, 150); + } + + // CONTEXT MENU HANDLING + + // To make the context menu work, we need to briefly unhide the + // textarea (making it as unobtrusive as possible) to let the + // right-click take effect on it. + function onContextMenu(cm, e) { + if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) return; + if (signalDOMEvent(cm, e, "contextmenu")) return; + cm.display.input.onContextMenu(e); + } + + function contextMenuInGutter(cm, e) { + if (!hasHandler(cm, "gutterContextMenu")) return false; + return gutterEvent(cm, e, "gutterContextMenu", false); + } + + // UPDATING + + // Compute the position of the end of a change (its 'to' property + // refers to the pre-change end). + var changeEnd = CodeMirror.changeEnd = function(change) { + if (!change.text) return change.to; + return Pos(change.from.line + change.text.length - 1, + lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0)); + }; + + // Adjust a position to refer to the post-change position of the + // same text, or the end of the change if the change covers it. + function adjustForChange(pos, change) { + if (cmp(pos, change.from) < 0) return pos; + if (cmp(pos, change.to) <= 0) return changeEnd(change); + + var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch; + if (pos.line == change.to.line) ch += changeEnd(change).ch - change.to.ch; + return Pos(line, ch); + } + + function computeSelAfterChange(doc, change) { + var out = []; + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + out.push(new Range(adjustForChange(range.anchor, change), + adjustForChange(range.head, change))); + } + return normalizeSelection(out, doc.sel.primIndex); + } + + function offsetPos(pos, old, nw) { + if (pos.line == old.line) + return Pos(nw.line, pos.ch - old.ch + nw.ch); + else + return Pos(nw.line + (pos.line - old.line), pos.ch); + } + + // Used by replaceSelections to allow moving the selection to the + // start or around the replaced test. Hint may be "start" or "around". + function computeReplacedSel(doc, changes, hint) { + var out = []; + var oldPrev = Pos(doc.first, 0), newPrev = oldPrev; + for (var i = 0; i < changes.length; i++) { + var change = changes[i]; + var from = offsetPos(change.from, oldPrev, newPrev); + var to = offsetPos(changeEnd(change), oldPrev, newPrev); + oldPrev = change.to; + newPrev = to; + if (hint == "around") { + var range = doc.sel.ranges[i], inv = cmp(range.head, range.anchor) < 0; + out[i] = new Range(inv ? to : from, inv ? from : to); + } else { + out[i] = new Range(from, from); + } + } + return new Selection(out, doc.sel.primIndex); + } + + // Allow "beforeChange" event handlers to influence a change + function filterChange(doc, change, update) { + var obj = { + canceled: false, + from: change.from, + to: change.to, + text: change.text, + origin: change.origin, + cancel: function() { this.canceled = true; } + }; + if (update) obj.update = function(from, to, text, origin) { + if (from) this.from = clipPos(doc, from); + if (to) this.to = clipPos(doc, to); + if (text) this.text = text; + if (origin !== undefined) this.origin = origin; + }; + signal(doc, "beforeChange", doc, obj); + if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj); + + if (obj.canceled) return null; + return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin}; + } + + // Apply a change to a document, and add it to the document's + // history, and propagating it to all linked documents. + function makeChange(doc, change, ignoreReadOnly) { + if (doc.cm) { + if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly); + if (doc.cm.state.suppressEdits) return; + } + + if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) { + change = filterChange(doc, change, true); + if (!change) return; + } + + // Possibly split or suppress the update based on the presence + // of read-only spans in its range. + var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to); + if (split) { + for (var i = split.length - 1; i >= 0; --i) + makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text}); + } else { + makeChangeInner(doc, change); + } + } + + function makeChangeInner(doc, change) { + if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) return; + var selAfter = computeSelAfterChange(doc, change); + addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN); + + makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change)); + var rebased = []; + + linkedDocs(doc, function(doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change)); + }); + } + + // Revert a change stored in a document's history. + function makeChangeFromHistory(doc, type, allowSelectionOnly) { + if (doc.cm && doc.cm.state.suppressEdits) return; + + var hist = doc.history, event, selAfter = doc.sel; + var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done; + + // Verify that there is a useable event (so that ctrl-z won't + // needlessly clear selection events) + for (var i = 0; i < source.length; i++) { + event = source[i]; + if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges) + break; + } + if (i == source.length) return; + hist.lastOrigin = hist.lastSelOrigin = null; + + for (;;) { + event = source.pop(); + if (event.ranges) { + pushSelectionToHistory(event, dest); + if (allowSelectionOnly && !event.equals(doc.sel)) { + setSelection(doc, event, {clearRedo: false}); + return; + } + selAfter = event; + } + else break; + } + + // Build up a reverse change object to add to the opposite history + // stack (redo when undoing, and vice versa). + var antiChanges = []; + pushSelectionToHistory(selAfter, dest); + dest.push({changes: antiChanges, generation: hist.generation}); + hist.generation = event.generation || ++hist.maxGeneration; + + var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange"); + + for (var i = event.changes.length - 1; i >= 0; --i) { + var change = event.changes[i]; + change.origin = type; + if (filter && !filterChange(doc, change, false)) { + source.length = 0; + return; + } + + antiChanges.push(historyChangeFromChange(doc, change)); + + var after = i ? computeSelAfterChange(doc, change) : lst(source); + makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change)); + if (!i && doc.cm) doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)}); + var rebased = []; + + // Propagate to the linked documents + linkedDocs(doc, function(doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change)); + }); + } + } + + // Sub-views need their line numbers shifted when text is added + // above or below them in the parent document. + function shiftDoc(doc, distance) { + if (distance == 0) return; + doc.first += distance; + doc.sel = new Selection(map(doc.sel.ranges, function(range) { + return new Range(Pos(range.anchor.line + distance, range.anchor.ch), + Pos(range.head.line + distance, range.head.ch)); + }), doc.sel.primIndex); + if (doc.cm) { + regChange(doc.cm, doc.first, doc.first - distance, distance); + for (var d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++) + regLineChange(doc.cm, l, "gutter"); + } + } + + // More lower-level change function, handling only a single document + // (not linked ones). + function makeChangeSingleDoc(doc, change, selAfter, spans) { + if (doc.cm && !doc.cm.curOp) + return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans); + + if (change.to.line < doc.first) { + shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line)); + return; + } + if (change.from.line > doc.lastLine()) return; + + // Clip the change to the size of this doc + if (change.from.line < doc.first) { + var shift = change.text.length - 1 - (doc.first - change.from.line); + shiftDoc(doc, shift); + change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch), + text: [lst(change.text)], origin: change.origin}; + } + var last = doc.lastLine(); + if (change.to.line > last) { + change = {from: change.from, to: Pos(last, getLine(doc, last).text.length), + text: [change.text[0]], origin: change.origin}; + } + + change.removed = getBetween(doc, change.from, change.to); + + if (!selAfter) selAfter = computeSelAfterChange(doc, change); + if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans); + else updateDoc(doc, change, spans); + setSelectionNoUndo(doc, selAfter, sel_dontScroll); + } + + // Handle the interaction of a change to a document with the editor + // that this document is part of. + function makeChangeSingleDocInEditor(cm, change, spans) { + var doc = cm.doc, display = cm.display, from = change.from, to = change.to; + + var recomputeMaxLength = false, checkWidthStart = from.line; + if (!cm.options.lineWrapping) { + checkWidthStart = lineNo(visualLine(getLine(doc, from.line))); + doc.iter(checkWidthStart, to.line + 1, function(line) { + if (line == display.maxLine) { + recomputeMaxLength = true; + return true; + } + }); + } + + if (doc.sel.contains(change.from, change.to) > -1) + signalCursorActivity(cm); + + updateDoc(doc, change, spans, estimateHeight(cm)); + + if (!cm.options.lineWrapping) { + doc.iter(checkWidthStart, from.line + change.text.length, function(line) { + var len = lineLength(line); + if (len > display.maxLineLength) { + display.maxLine = line; + display.maxLineLength = len; + display.maxLineChanged = true; + recomputeMaxLength = false; + } + }); + if (recomputeMaxLength) cm.curOp.updateMaxLine = true; + } + + // Adjust frontier, schedule worker + doc.frontier = Math.min(doc.frontier, from.line); + startWorker(cm, 400); + + var lendiff = change.text.length - (to.line - from.line) - 1; + // Remember that these lines changed, for updating the display + if (change.full) + regChange(cm); + else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change)) + regLineChange(cm, from.line, "text"); + else + regChange(cm, from.line, to.line + 1, lendiff); + + var changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change"); + if (changeHandler || changesHandler) { + var obj = { + from: from, to: to, + text: change.text, + removed: change.removed, + origin: change.origin + }; + if (changeHandler) signalLater(cm, "change", cm, obj); + if (changesHandler) (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj); + } + cm.display.selForContextMenu = null; + } + + function replaceRange(doc, code, from, to, origin) { + if (!to) to = from; + if (cmp(to, from) < 0) { var tmp = to; to = from; from = tmp; } + if (typeof code == "string") code = doc.splitLines(code); + makeChange(doc, {from: from, to: to, text: code, origin: origin}); + } + + // SCROLLING THINGS INTO VIEW + + // If an editor sits on the top or bottom of the window, partially + // scrolled out of view, this ensures that the cursor is visible. + function maybeScrollWindow(cm, coords) { + if (signalDOMEvent(cm, "scrollCursorIntoView")) return; + + var display = cm.display, box = display.sizer.getBoundingClientRect(), doScroll = null; + if (coords.top + box.top < 0) doScroll = true; + else if (coords.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false; + if (doScroll != null && !phantom) { + var scrollNode = elt("div", "\u200b", null, "position: absolute; top: " + + (coords.top - display.viewOffset - paddingTop(cm.display)) + "px; height: " + + (coords.bottom - coords.top + scrollGap(cm) + display.barHeight) + "px; left: " + + coords.left + "px; width: 2px;"); + cm.display.lineSpace.appendChild(scrollNode); + scrollNode.scrollIntoView(doScroll); + cm.display.lineSpace.removeChild(scrollNode); + } + } + + // Scroll a given position into view (immediately), verifying that + // it actually became visible (as line heights are accurately + // measured, the position of something may 'drift' during drawing). + function scrollPosIntoView(cm, pos, end, margin) { + if (margin == null) margin = 0; + for (var limit = 0; limit < 5; limit++) { + var changed = false, coords = cursorCoords(cm, pos); + var endCoords = !end || end == pos ? coords : cursorCoords(cm, end); + var scrollPos = calculateScrollPos(cm, Math.min(coords.left, endCoords.left), + Math.min(coords.top, endCoords.top) - margin, + Math.max(coords.left, endCoords.left), + Math.max(coords.bottom, endCoords.bottom) + margin); + var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft; + if (scrollPos.scrollTop != null) { + setScrollTop(cm, scrollPos.scrollTop); + if (Math.abs(cm.doc.scrollTop - startTop) > 1) changed = true; + } + if (scrollPos.scrollLeft != null) { + setScrollLeft(cm, scrollPos.scrollLeft); + if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) changed = true; + } + if (!changed) break; + } + return coords; + } + + // Scroll a given set of coordinates into view (immediately). + function scrollIntoView(cm, x1, y1, x2, y2) { + var scrollPos = calculateScrollPos(cm, x1, y1, x2, y2); + if (scrollPos.scrollTop != null) setScrollTop(cm, scrollPos.scrollTop); + if (scrollPos.scrollLeft != null) setScrollLeft(cm, scrollPos.scrollLeft); + } + + // Calculate a new scroll position needed to scroll the given + // rectangle into view. Returns an object with scrollTop and + // scrollLeft properties. When these are undefined, the + // vertical/horizontal position does not need to be adjusted. + function calculateScrollPos(cm, x1, y1, x2, y2) { + var display = cm.display, snapMargin = textHeight(cm.display); + if (y1 < 0) y1 = 0; + var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop; + var screen = displayHeight(cm), result = {}; + if (y2 - y1 > screen) y2 = y1 + screen; + var docBottom = cm.doc.height + paddingVert(display); + var atTop = y1 < snapMargin, atBottom = y2 > docBottom - snapMargin; + if (y1 < screentop) { + result.scrollTop = atTop ? 0 : y1; + } else if (y2 > screentop + screen) { + var newTop = Math.min(y1, (atBottom ? docBottom : y2) - screen); + if (newTop != screentop) result.scrollTop = newTop; + } + + var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft; + var screenw = displayWidth(cm) - (cm.options.fixedGutter ? display.gutters.offsetWidth : 0); + var tooWide = x2 - x1 > screenw; + if (tooWide) x2 = x1 + screenw; + if (x1 < 10) + result.scrollLeft = 0; + else if (x1 < screenleft) + result.scrollLeft = Math.max(0, x1 - (tooWide ? 0 : 10)); + else if (x2 > screenw + screenleft - 3) + result.scrollLeft = x2 + (tooWide ? 0 : 10) - screenw; + return result; + } + + // Store a relative adjustment to the scroll position in the current + // operation (to be applied when the operation finishes). + function addToScrollPos(cm, left, top) { + if (left != null || top != null) resolveScrollToPos(cm); + if (left != null) + cm.curOp.scrollLeft = (cm.curOp.scrollLeft == null ? cm.doc.scrollLeft : cm.curOp.scrollLeft) + left; + if (top != null) + cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top; + } + + // Make sure that at the end of the operation the current cursor is + // shown. + function ensureCursorVisible(cm) { + resolveScrollToPos(cm); + var cur = cm.getCursor(), from = cur, to = cur; + if (!cm.options.lineWrapping) { + from = cur.ch ? Pos(cur.line, cur.ch - 1) : cur; + to = Pos(cur.line, cur.ch + 1); + } + cm.curOp.scrollToPos = {from: from, to: to, margin: cm.options.cursorScrollMargin, isCursor: true}; + } + + // When an operation has its scrollToPos property set, and another + // scroll action is applied before the end of the operation, this + // 'simulates' scrolling that position into view in a cheap way, so + // that the effect of intermediate scroll commands is not ignored. + function resolveScrollToPos(cm) { + var range = cm.curOp.scrollToPos; + if (range) { + cm.curOp.scrollToPos = null; + var from = estimateCoords(cm, range.from), to = estimateCoords(cm, range.to); + var sPos = calculateScrollPos(cm, Math.min(from.left, to.left), + Math.min(from.top, to.top) - range.margin, + Math.max(from.right, to.right), + Math.max(from.bottom, to.bottom) + range.margin); + cm.scrollTo(sPos.scrollLeft, sPos.scrollTop); + } + } + + // API UTILITIES + + // Indent the given line. The how parameter can be "smart", + // "add"/null, "subtract", or "prev". When aggressive is false + // (typically set to true for forced single-line indents), empty + // lines are not indented, and places where the mode returns Pass + // are left alone. + function indentLine(cm, n, how, aggressive) { + var doc = cm.doc, state; + if (how == null) how = "add"; + if (how == "smart") { + // Fall back to "prev" when the mode doesn't have an indentation + // method. + if (!doc.mode.indent) how = "prev"; + else state = getStateBefore(cm, n); + } + + var tabSize = cm.options.tabSize; + var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize); + if (line.stateAfter) line.stateAfter = null; + var curSpaceString = line.text.match(/^\s*/)[0], indentation; + if (!aggressive && !/\S/.test(line.text)) { + indentation = 0; + how = "not"; + } else if (how == "smart") { + indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text); + if (indentation == Pass || indentation > 150) { + if (!aggressive) return; + how = "prev"; + } + } + if (how == "prev") { + if (n > doc.first) indentation = countColumn(getLine(doc, n-1).text, null, tabSize); + else indentation = 0; + } else if (how == "add") { + indentation = curSpace + cm.options.indentUnit; + } else if (how == "subtract") { + indentation = curSpace - cm.options.indentUnit; + } else if (typeof how == "number") { + indentation = curSpace + how; + } + indentation = Math.max(0, indentation); + + var indentString = "", pos = 0; + if (cm.options.indentWithTabs) + for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";} + if (pos < indentation) indentString += spaceStr(indentation - pos); + + if (indentString != curSpaceString) { + replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input"); + line.stateAfter = null; + return true; + } else { + // Ensure that, if the cursor was in the whitespace at the start + // of the line, it is moved to the end of that space. + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + if (range.head.line == n && range.head.ch < curSpaceString.length) { + var pos = Pos(n, curSpaceString.length); + replaceOneSelection(doc, i, new Range(pos, pos)); + break; + } + } + } + } + + // Utility for applying a change to a line by handle or number, + // returning the number and optionally registering the line as + // changed. + function changeLine(doc, handle, changeType, op) { + var no = handle, line = handle; + if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle)); + else no = lineNo(handle); + if (no == null) return null; + if (op(line, no) && doc.cm) regLineChange(doc.cm, no, changeType); + return line; + } + + // Helper for deleting text near the selection(s), used to implement + // backspace, delete, and similar functionality. + function deleteNearSelection(cm, compute) { + var ranges = cm.doc.sel.ranges, kill = []; + // Build up a set of ranges to kill first, merging overlapping + // ranges. + for (var i = 0; i < ranges.length; i++) { + var toKill = compute(ranges[i]); + while (kill.length && cmp(toKill.from, lst(kill).to) <= 0) { + var replaced = kill.pop(); + if (cmp(replaced.from, toKill.from) < 0) { + toKill.from = replaced.from; + break; + } + } + kill.push(toKill); + } + // Next, remove those actual ranges. + runInOp(cm, function() { + for (var i = kill.length - 1; i >= 0; i--) + replaceRange(cm.doc, "", kill[i].from, kill[i].to, "+delete"); + ensureCursorVisible(cm); + }); + } + + // Used for horizontal relative motion. Dir is -1 or 1 (left or + // right), unit can be "char", "column" (like char, but doesn't + // cross line boundaries), "word" (across next word), or "group" (to + // the start of next group of word or non-word-non-whitespace + // chars). The visually param controls whether, in right-to-left + // text, direction 1 means to move towards the next index in the + // string, or towards the character to the right of the current + // position. The resulting position will have a hitSide=true + // property if it reached the end of the document. + function findPosH(doc, pos, dir, unit, visually) { + var line = pos.line, ch = pos.ch, origDir = dir; + var lineObj = getLine(doc, line); + function findNextLine() { + var l = line + dir; + if (l < doc.first || l >= doc.first + doc.size) return false + line = l; + return lineObj = getLine(doc, l); + } + function moveOnce(boundToLine) { + var next = (visually ? moveVisually : moveLogically)(lineObj, ch, dir, true); + if (next == null) { + if (!boundToLine && findNextLine()) { + if (visually) ch = (dir < 0 ? lineRight : lineLeft)(lineObj); + else ch = dir < 0 ? lineObj.text.length : 0; + } else return false + } else ch = next; + return true; + } + + if (unit == "char") { + moveOnce() + } else if (unit == "column") { + moveOnce(true) + } else if (unit == "word" || unit == "group") { + var sawType = null, group = unit == "group"; + var helper = doc.cm && doc.cm.getHelper(pos, "wordChars"); + for (var first = true;; first = false) { + if (dir < 0 && !moveOnce(!first)) break; + var cur = lineObj.text.charAt(ch) || "\n"; + var type = isWordChar(cur, helper) ? "w" + : group && cur == "\n" ? "n" + : !group || /\s/.test(cur) ? null + : "p"; + if (group && !first && !type) type = "s"; + if (sawType && sawType != type) { + if (dir < 0) {dir = 1; moveOnce();} + break; + } + + if (type) sawType = type; + if (dir > 0 && !moveOnce(!first)) break; + } + } + var result = skipAtomic(doc, Pos(line, ch), pos, origDir, true); + if (!cmp(pos, result)) result.hitSide = true; + return result; + } + + // For relative vertical movement. Dir may be -1 or 1. Unit can be + // "page" or "line". The resulting position will have a hitSide=true + // property if it reached the end of the document. + function findPosV(cm, pos, dir, unit) { + var doc = cm.doc, x = pos.left, y; + if (unit == "page") { + var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight); + y = pos.top + dir * (pageSize - (dir < 0 ? 1.5 : .5) * textHeight(cm.display)); + } else if (unit == "line") { + y = dir > 0 ? pos.bottom + 3 : pos.top - 3; + } + for (;;) { + var target = coordsChar(cm, x, y); + if (!target.outside) break; + if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break; } + y += dir * 5; + } + return target; + } + + // EDITOR METHODS + + // The publicly visible API. Note that methodOp(f) means + // 'wrap f in an operation, performed on its `this` parameter'. + + // This is not the complete set of editor methods. Most of the + // methods defined on the Doc type are also injected into + // CodeMirror.prototype, for backwards compatibility and + // convenience. + + CodeMirror.prototype = { + constructor: CodeMirror, + focus: function(){window.focus(); this.display.input.focus();}, + + setOption: function(option, value) { + var options = this.options, old = options[option]; + if (options[option] == value && option != "mode") return; + options[option] = value; + if (optionHandlers.hasOwnProperty(option)) + operation(this, optionHandlers[option])(this, value, old); + }, + + getOption: function(option) {return this.options[option];}, + getDoc: function() {return this.doc;}, + + addKeyMap: function(map, bottom) { + this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map)); + }, + removeKeyMap: function(map) { + var maps = this.state.keyMaps; + for (var i = 0; i < maps.length; ++i) + if (maps[i] == map || maps[i].name == map) { + maps.splice(i, 1); + return true; + } + }, + + addOverlay: methodOp(function(spec, options) { + var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec); + if (mode.startState) throw new Error("Overlays may not be stateful."); + this.state.overlays.push({mode: mode, modeSpec: spec, opaque: options && options.opaque}); + this.state.modeGen++; + regChange(this); + }), + removeOverlay: methodOp(function(spec) { + var overlays = this.state.overlays; + for (var i = 0; i < overlays.length; ++i) { + var cur = overlays[i].modeSpec; + if (cur == spec || typeof spec == "string" && cur.name == spec) { + overlays.splice(i, 1); + this.state.modeGen++; + regChange(this); + return; + } + } + }), + + indentLine: methodOp(function(n, dir, aggressive) { + if (typeof dir != "string" && typeof dir != "number") { + if (dir == null) dir = this.options.smartIndent ? "smart" : "prev"; + else dir = dir ? "add" : "subtract"; + } + if (isLine(this.doc, n)) indentLine(this, n, dir, aggressive); + }), + indentSelection: methodOp(function(how) { + var ranges = this.doc.sel.ranges, end = -1; + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + if (!range.empty()) { + var from = range.from(), to = range.to(); + var start = Math.max(end, from.line); + end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1; + for (var j = start; j < end; ++j) + indentLine(this, j, how); + var newRanges = this.doc.sel.ranges; + if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0) + replaceOneSelection(this.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll); + } else if (range.head.line > end) { + indentLine(this, range.head.line, how, true); + end = range.head.line; + if (i == this.doc.sel.primIndex) ensureCursorVisible(this); + } + } + }), + + // Fetch the parser token for a given character. Useful for hacks + // that want to inspect the mode state (say, for completion). + getTokenAt: function(pos, precise) { + return takeToken(this, pos, precise); + }, + + getLineTokens: function(line, precise) { + return takeToken(this, Pos(line), precise, true); + }, + + getTokenTypeAt: function(pos) { + pos = clipPos(this.doc, pos); + var styles = getLineStyles(this, getLine(this.doc, pos.line)); + var before = 0, after = (styles.length - 1) / 2, ch = pos.ch; + var type; + if (ch == 0) type = styles[2]; + else for (;;) { + var mid = (before + after) >> 1; + if ((mid ? styles[mid * 2 - 1] : 0) >= ch) after = mid; + else if (styles[mid * 2 + 1] < ch) before = mid + 1; + else { type = styles[mid * 2 + 2]; break; } + } + var cut = type ? type.indexOf("cm-overlay ") : -1; + return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1); + }, + + getModeAt: function(pos) { + var mode = this.doc.mode; + if (!mode.innerMode) return mode; + return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode; + }, + + getHelper: function(pos, type) { + return this.getHelpers(pos, type)[0]; + }, + + getHelpers: function(pos, type) { + var found = []; + if (!helpers.hasOwnProperty(type)) return found; + var help = helpers[type], mode = this.getModeAt(pos); + if (typeof mode[type] == "string") { + if (help[mode[type]]) found.push(help[mode[type]]); + } else if (mode[type]) { + for (var i = 0; i < mode[type].length; i++) { + var val = help[mode[type][i]]; + if (val) found.push(val); + } + } else if (mode.helperType && help[mode.helperType]) { + found.push(help[mode.helperType]); + } else if (help[mode.name]) { + found.push(help[mode.name]); + } + for (var i = 0; i < help._global.length; i++) { + var cur = help._global[i]; + if (cur.pred(mode, this) && indexOf(found, cur.val) == -1) + found.push(cur.val); + } + return found; + }, + + getStateAfter: function(line, precise) { + var doc = this.doc; + line = clipLine(doc, line == null ? doc.first + doc.size - 1: line); + return getStateBefore(this, line + 1, precise); + }, + + cursorCoords: function(start, mode) { + var pos, range = this.doc.sel.primary(); + if (start == null) pos = range.head; + else if (typeof start == "object") pos = clipPos(this.doc, start); + else pos = start ? range.from() : range.to(); + return cursorCoords(this, pos, mode || "page"); + }, + + charCoords: function(pos, mode) { + return charCoords(this, clipPos(this.doc, pos), mode || "page"); + }, + + coordsChar: function(coords, mode) { + coords = fromCoordSystem(this, coords, mode || "page"); + return coordsChar(this, coords.left, coords.top); + }, + + lineAtHeight: function(height, mode) { + height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top; + return lineAtHeight(this.doc, height + this.display.viewOffset); + }, + heightAtLine: function(line, mode) { + var end = false, lineObj; + if (typeof line == "number") { + var last = this.doc.first + this.doc.size - 1; + if (line < this.doc.first) line = this.doc.first; + else if (line > last) { line = last; end = true; } + lineObj = getLine(this.doc, line); + } else { + lineObj = line; + } + return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page").top + + (end ? this.doc.height - heightAtLine(lineObj) : 0); + }, + + defaultTextHeight: function() { return textHeight(this.display); }, + defaultCharWidth: function() { return charWidth(this.display); }, + + setGutterMarker: methodOp(function(line, gutterID, value) { + return changeLine(this.doc, line, "gutter", function(line) { + var markers = line.gutterMarkers || (line.gutterMarkers = {}); + markers[gutterID] = value; + if (!value && isEmpty(markers)) line.gutterMarkers = null; + return true; + }); + }), + + clearGutter: methodOp(function(gutterID) { + var cm = this, doc = cm.doc, i = doc.first; + doc.iter(function(line) { + if (line.gutterMarkers && line.gutterMarkers[gutterID]) { + line.gutterMarkers[gutterID] = null; + regLineChange(cm, i, "gutter"); + if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null; + } + ++i; + }); + }), + + lineInfo: function(line) { + if (typeof line == "number") { + if (!isLine(this.doc, line)) return null; + var n = line; + line = getLine(this.doc, line); + if (!line) return null; + } else { + var n = lineNo(line); + if (n == null) return null; + } + return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers, + textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass, + widgets: line.widgets}; + }, + + getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo};}, + + addWidget: function(pos, node, scroll, vert, horiz) { + var display = this.display; + pos = cursorCoords(this, clipPos(this.doc, pos)); + var top = pos.bottom, left = pos.left; + node.style.position = "absolute"; + node.setAttribute("cm-ignore-events", "true"); + this.display.input.setUneditable(node); + display.sizer.appendChild(node); + if (vert == "over") { + top = pos.top; + } else if (vert == "above" || vert == "near") { + var vspace = Math.max(display.wrapper.clientHeight, this.doc.height), + hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth); + // Default to positioning above (if specified and possible); otherwise default to positioning below + if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight) + top = pos.top - node.offsetHeight; + else if (pos.bottom + node.offsetHeight <= vspace) + top = pos.bottom; + if (left + node.offsetWidth > hspace) + left = hspace - node.offsetWidth; + } + node.style.top = top + "px"; + node.style.left = node.style.right = ""; + if (horiz == "right") { + left = display.sizer.clientWidth - node.offsetWidth; + node.style.right = "0px"; + } else { + if (horiz == "left") left = 0; + else if (horiz == "middle") left = (display.sizer.clientWidth - node.offsetWidth) / 2; + node.style.left = left + "px"; + } + if (scroll) + scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight); + }, + + triggerOnKeyDown: methodOp(onKeyDown), + triggerOnKeyPress: methodOp(onKeyPress), + triggerOnKeyUp: onKeyUp, + + execCommand: function(cmd) { + if (commands.hasOwnProperty(cmd)) + return commands[cmd].call(null, this); + }, + + triggerElectric: methodOp(function(text) { triggerElectric(this, text); }), + + findPosH: function(from, amount, unit, visually) { + var dir = 1; + if (amount < 0) { dir = -1; amount = -amount; } + for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { + cur = findPosH(this.doc, cur, dir, unit, visually); + if (cur.hitSide) break; + } + return cur; + }, + + moveH: methodOp(function(dir, unit) { + var cm = this; + cm.extendSelectionsBy(function(range) { + if (cm.display.shift || cm.doc.extend || range.empty()) + return findPosH(cm.doc, range.head, dir, unit, cm.options.rtlMoveVisually); + else + return dir < 0 ? range.from() : range.to(); + }, sel_move); + }), + + deleteH: methodOp(function(dir, unit) { + var sel = this.doc.sel, doc = this.doc; + if (sel.somethingSelected()) + doc.replaceSelection("", null, "+delete"); + else + deleteNearSelection(this, function(range) { + var other = findPosH(doc, range.head, dir, unit, false); + return dir < 0 ? {from: other, to: range.head} : {from: range.head, to: other}; + }); + }), + + findPosV: function(from, amount, unit, goalColumn) { + var dir = 1, x = goalColumn; + if (amount < 0) { dir = -1; amount = -amount; } + for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { + var coords = cursorCoords(this, cur, "div"); + if (x == null) x = coords.left; + else coords.left = x; + cur = findPosV(this, coords, dir, unit); + if (cur.hitSide) break; + } + return cur; + }, + + moveV: methodOp(function(dir, unit) { + var cm = this, doc = this.doc, goals = []; + var collapse = !cm.display.shift && !doc.extend && doc.sel.somethingSelected(); + doc.extendSelectionsBy(function(range) { + if (collapse) + return dir < 0 ? range.from() : range.to(); + var headPos = cursorCoords(cm, range.head, "div"); + if (range.goalColumn != null) headPos.left = range.goalColumn; + goals.push(headPos.left); + var pos = findPosV(cm, headPos, dir, unit); + if (unit == "page" && range == doc.sel.primary()) + addToScrollPos(cm, null, charCoords(cm, pos, "div").top - headPos.top); + return pos; + }, sel_move); + if (goals.length) for (var i = 0; i < doc.sel.ranges.length; i++) + doc.sel.ranges[i].goalColumn = goals[i]; + }), + + // Find the word at the given position (as returned by coordsChar). + findWordAt: function(pos) { + var doc = this.doc, line = getLine(doc, pos.line).text; + var start = pos.ch, end = pos.ch; + if (line) { + var helper = this.getHelper(pos, "wordChars"); + if ((pos.xRel < 0 || end == line.length) && start) --start; else ++end; + var startChar = line.charAt(start); + var check = isWordChar(startChar, helper) + ? function(ch) { return isWordChar(ch, helper); } + : /\s/.test(startChar) ? function(ch) {return /\s/.test(ch);} + : function(ch) {return !/\s/.test(ch) && !isWordChar(ch);}; + while (start > 0 && check(line.charAt(start - 1))) --start; + while (end < line.length && check(line.charAt(end))) ++end; + } + return new Range(Pos(pos.line, start), Pos(pos.line, end)); + }, + + toggleOverwrite: function(value) { + if (value != null && value == this.state.overwrite) return; + if (this.state.overwrite = !this.state.overwrite) + addClass(this.display.cursorDiv, "CodeMirror-overwrite"); + else + rmClass(this.display.cursorDiv, "CodeMirror-overwrite"); + + signal(this, "overwriteToggle", this, this.state.overwrite); + }, + hasFocus: function() { return this.display.input.getField() == activeElt(); }, + isReadOnly: function() { return !!(this.options.readOnly || this.doc.cantEdit); }, + + scrollTo: methodOp(function(x, y) { + if (x != null || y != null) resolveScrollToPos(this); + if (x != null) this.curOp.scrollLeft = x; + if (y != null) this.curOp.scrollTop = y; + }), + getScrollInfo: function() { + var scroller = this.display.scroller; + return {left: scroller.scrollLeft, top: scroller.scrollTop, + height: scroller.scrollHeight - scrollGap(this) - this.display.barHeight, + width: scroller.scrollWidth - scrollGap(this) - this.display.barWidth, + clientHeight: displayHeight(this), clientWidth: displayWidth(this)}; + }, + + scrollIntoView: methodOp(function(range, margin) { + if (range == null) { + range = {from: this.doc.sel.primary().head, to: null}; + if (margin == null) margin = this.options.cursorScrollMargin; + } else if (typeof range == "number") { + range = {from: Pos(range, 0), to: null}; + } else if (range.from == null) { + range = {from: range, to: null}; + } + if (!range.to) range.to = range.from; + range.margin = margin || 0; + + if (range.from.line != null) { + resolveScrollToPos(this); + this.curOp.scrollToPos = range; + } else { + var sPos = calculateScrollPos(this, Math.min(range.from.left, range.to.left), + Math.min(range.from.top, range.to.top) - range.margin, + Math.max(range.from.right, range.to.right), + Math.max(range.from.bottom, range.to.bottom) + range.margin); + this.scrollTo(sPos.scrollLeft, sPos.scrollTop); + } + }), + + setSize: methodOp(function(width, height) { + var cm = this; + function interpret(val) { + return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val; + } + if (width != null) cm.display.wrapper.style.width = interpret(width); + if (height != null) cm.display.wrapper.style.height = interpret(height); + if (cm.options.lineWrapping) clearLineMeasurementCache(this); + var lineNo = cm.display.viewFrom; + cm.doc.iter(lineNo, cm.display.viewTo, function(line) { + if (line.widgets) for (var i = 0; i < line.widgets.length; i++) + if (line.widgets[i].noHScroll) { regLineChange(cm, lineNo, "widget"); break; } + ++lineNo; + }); + cm.curOp.forceUpdate = true; + signal(cm, "refresh", this); + }), + + operation: function(f){return runInOp(this, f);}, + + refresh: methodOp(function() { + var oldHeight = this.display.cachedTextHeight; + regChange(this); + this.curOp.forceUpdate = true; + clearCaches(this); + this.scrollTo(this.doc.scrollLeft, this.doc.scrollTop); + updateGutterSpace(this); + if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5) + estimateLineHeights(this); + signal(this, "refresh", this); + }), + + swapDoc: methodOp(function(doc) { + var old = this.doc; + old.cm = null; + attachDoc(this, doc); + clearCaches(this); + this.display.input.reset(); + this.scrollTo(doc.scrollLeft, doc.scrollTop); + this.curOp.forceScroll = true; + signalLater(this, "swapDoc", this, old); + return old; + }), + + getInputField: function(){return this.display.input.getField();}, + getWrapperElement: function(){return this.display.wrapper;}, + getScrollerElement: function(){return this.display.scroller;}, + getGutterElement: function(){return this.display.gutters;} + }; + eventMixin(CodeMirror); + + // OPTION DEFAULTS + + // The default configuration options. + var defaults = CodeMirror.defaults = {}; + // Functions to run when options are changed. + var optionHandlers = CodeMirror.optionHandlers = {}; + + function option(name, deflt, handle, notOnInit) { + CodeMirror.defaults[name] = deflt; + if (handle) optionHandlers[name] = + notOnInit ? function(cm, val, old) {if (old != Init) handle(cm, val, old);} : handle; + } + + // Passed to option handlers when there is no old value. + var Init = CodeMirror.Init = {toString: function(){return "CodeMirror.Init";}}; + + // These two are, on init, called from the constructor because they + // have to be initialized before the editor can start at all. + option("value", "", function(cm, val) { + cm.setValue(val); + }, true); + option("mode", null, function(cm, val) { + cm.doc.modeOption = val; + loadMode(cm); + }, true); + + option("indentUnit", 2, loadMode, true); + option("indentWithTabs", false); + option("smartIndent", true); + option("tabSize", 4, function(cm) { + resetModeState(cm); + clearCaches(cm); + regChange(cm); + }, true); + option("lineSeparator", null, function(cm, val) { + cm.doc.lineSep = val; + if (!val) return; + var newBreaks = [], lineNo = cm.doc.first; + cm.doc.iter(function(line) { + for (var pos = 0;;) { + var found = line.text.indexOf(val, pos); + if (found == -1) break; + pos = found + val.length; + newBreaks.push(Pos(lineNo, found)); + } + lineNo++; + }); + for (var i = newBreaks.length - 1; i >= 0; i--) + replaceRange(cm.doc, val, newBreaks[i], Pos(newBreaks[i].line, newBreaks[i].ch + val.length)) + }); + option("specialChars", /[\u0000-\u001f\u007f\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g, function(cm, val, old) { + cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g"); + if (old != CodeMirror.Init) cm.refresh(); + }); + option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function(cm) {cm.refresh();}, true); + option("electricChars", true); + option("inputStyle", mobile ? "contenteditable" : "textarea", function() { + throw new Error("inputStyle can not (yet) be changed in a running editor"); // FIXME + }, true); + option("rtlMoveVisually", !windows); + option("wholeLineUpdateBefore", true); + + option("theme", "default", function(cm) { + themeChanged(cm); + guttersChanged(cm); + }, true); + option("keyMap", "default", function(cm, val, old) { + var next = getKeyMap(val); + var prev = old != CodeMirror.Init && getKeyMap(old); + if (prev && prev.detach) prev.detach(cm, next); + if (next.attach) next.attach(cm, prev || null); + }); + option("extraKeys", null); + + option("lineWrapping", false, wrappingChanged, true); + option("gutters", [], function(cm) { + setGuttersForLineNumbers(cm.options); + guttersChanged(cm); + }, true); + option("fixedGutter", true, function(cm, val) { + cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0"; + cm.refresh(); + }, true); + option("coverGutterNextToScrollbar", false, function(cm) {updateScrollbars(cm);}, true); + option("scrollbarStyle", "native", function(cm) { + initScrollbars(cm); + updateScrollbars(cm); + cm.display.scrollbars.setScrollTop(cm.doc.scrollTop); + cm.display.scrollbars.setScrollLeft(cm.doc.scrollLeft); + }, true); + option("lineNumbers", false, function(cm) { + setGuttersForLineNumbers(cm.options); + guttersChanged(cm); + }, true); + option("firstLineNumber", 1, guttersChanged, true); + option("lineNumberFormatter", function(integer) {return integer;}, guttersChanged, true); + option("showCursorWhenSelecting", false, updateSelection, true); + + option("resetSelectionOnContextMenu", true); + option("lineWiseCopyCut", true); + + option("readOnly", false, function(cm, val) { + if (val == "nocursor") { + onBlur(cm); + cm.display.input.blur(); + cm.display.disabled = true; + } else { + cm.display.disabled = false; + } + cm.display.input.readOnlyChanged(val) + }); + option("disableInput", false, function(cm, val) {if (!val) cm.display.input.reset();}, true); + option("dragDrop", true, dragDropChanged); + option("allowDropFileTypes", null); + + option("cursorBlinkRate", 530); + option("cursorScrollMargin", 0); + option("cursorHeight", 1, updateSelection, true); + option("singleCursorHeightPerLine", true, updateSelection, true); + option("workTime", 100); + option("workDelay", 100); + option("flattenSpans", true, resetModeState, true); + option("addModeClass", false, resetModeState, true); + option("pollInterval", 100); + option("undoDepth", 200, function(cm, val){cm.doc.history.undoDepth = val;}); + option("historyEventDelay", 1250); + option("viewportMargin", 10, function(cm){cm.refresh();}, true); + option("maxHighlightLength", 10000, resetModeState, true); + option("moveInputWithCursor", true, function(cm, val) { + if (!val) cm.display.input.resetPosition(); + }); + + option("tabindex", null, function(cm, val) { + cm.display.input.getField().tabIndex = val || ""; + }); + option("autofocus", null); + + // MODE DEFINITION AND QUERYING + + // Known modes, by name and by MIME + var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {}; + + // Extra arguments are stored as the mode's dependencies, which is + // used by (legacy) mechanisms like loadmode.js to automatically + // load a mode. (Preferred mechanism is the require/define calls.) + CodeMirror.defineMode = function(name, mode) { + if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name; + if (arguments.length > 2) + mode.dependencies = Array.prototype.slice.call(arguments, 2); + modes[name] = mode; + }; + + CodeMirror.defineMIME = function(mime, spec) { + mimeModes[mime] = spec; + }; + + // Given a MIME type, a {name, ...options} config object, or a name + // string, return a mode config object. + CodeMirror.resolveMode = function(spec) { + if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) { + spec = mimeModes[spec]; + } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) { + var found = mimeModes[spec.name]; + if (typeof found == "string") found = {name: found}; + spec = createObj(found, spec); + spec.name = found.name; + } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) { + return CodeMirror.resolveMode("application/xml"); + } + if (typeof spec == "string") return {name: spec}; + else return spec || {name: "null"}; + }; + + // Given a mode spec (anything that resolveMode accepts), find and + // initialize an actual mode object. + CodeMirror.getMode = function(options, spec) { + var spec = CodeMirror.resolveMode(spec); + var mfactory = modes[spec.name]; + if (!mfactory) return CodeMirror.getMode(options, "text/plain"); + var modeObj = mfactory(options, spec); + if (modeExtensions.hasOwnProperty(spec.name)) { + var exts = modeExtensions[spec.name]; + for (var prop in exts) { + if (!exts.hasOwnProperty(prop)) continue; + if (modeObj.hasOwnProperty(prop)) modeObj["_" + prop] = modeObj[prop]; + modeObj[prop] = exts[prop]; + } + } + modeObj.name = spec.name; + if (spec.helperType) modeObj.helperType = spec.helperType; + if (spec.modeProps) for (var prop in spec.modeProps) + modeObj[prop] = spec.modeProps[prop]; + + return modeObj; + }; + + // Minimal default mode. + CodeMirror.defineMode("null", function() { + return {token: function(stream) {stream.skipToEnd();}}; + }); + CodeMirror.defineMIME("text/plain", "null"); + + // This can be used to attach properties to mode objects from + // outside the actual mode definition. + var modeExtensions = CodeMirror.modeExtensions = {}; + CodeMirror.extendMode = function(mode, properties) { + var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {}); + copyObj(properties, exts); + }; + + // EXTENSIONS + + CodeMirror.defineExtension = function(name, func) { + CodeMirror.prototype[name] = func; + }; + CodeMirror.defineDocExtension = function(name, func) { + Doc.prototype[name] = func; + }; + CodeMirror.defineOption = option; + + var initHooks = []; + CodeMirror.defineInitHook = function(f) {initHooks.push(f);}; + + var helpers = CodeMirror.helpers = {}; + CodeMirror.registerHelper = function(type, name, value) { + if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {_global: []}; + helpers[type][name] = value; + }; + CodeMirror.registerGlobalHelper = function(type, name, predicate, value) { + CodeMirror.registerHelper(type, name, value); + helpers[type]._global.push({pred: predicate, val: value}); + }; + + // MODE STATE HANDLING + + // Utility functions for working with state. Exported because nested + // modes need to do this for their inner modes. + + var copyState = CodeMirror.copyState = function(mode, state) { + if (state === true) return state; + if (mode.copyState) return mode.copyState(state); + var nstate = {}; + for (var n in state) { + var val = state[n]; + if (val instanceof Array) val = val.concat([]); + nstate[n] = val; + } + return nstate; + }; + + var startState = CodeMirror.startState = function(mode, a1, a2) { + return mode.startState ? mode.startState(a1, a2) : true; + }; + + // Given a mode and a state (for that mode), find the inner mode and + // state at the position that the state refers to. + CodeMirror.innerMode = function(mode, state) { + while (mode.innerMode) { + var info = mode.innerMode(state); + if (!info || info.mode == mode) break; + state = info.state; + mode = info.mode; + } + return info || {mode: mode, state: state}; + }; + + // STANDARD COMMANDS + + // Commands are parameter-less actions that can be performed on an + // editor, mostly used for keybindings. + var commands = CodeMirror.commands = { + selectAll: function(cm) {cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll);}, + singleSelection: function(cm) { + cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll); + }, + killLine: function(cm) { + deleteNearSelection(cm, function(range) { + if (range.empty()) { + var len = getLine(cm.doc, range.head.line).text.length; + if (range.head.ch == len && range.head.line < cm.lastLine()) + return {from: range.head, to: Pos(range.head.line + 1, 0)}; + else + return {from: range.head, to: Pos(range.head.line, len)}; + } else { + return {from: range.from(), to: range.to()}; + } + }); + }, + deleteLine: function(cm) { + deleteNearSelection(cm, function(range) { + return {from: Pos(range.from().line, 0), + to: clipPos(cm.doc, Pos(range.to().line + 1, 0))}; + }); + }, + delLineLeft: function(cm) { + deleteNearSelection(cm, function(range) { + return {from: Pos(range.from().line, 0), to: range.from()}; + }); + }, + delWrappedLineLeft: function(cm) { + deleteNearSelection(cm, function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + var leftPos = cm.coordsChar({left: 0, top: top}, "div"); + return {from: leftPos, to: range.from()}; + }); + }, + delWrappedLineRight: function(cm) { + deleteNearSelection(cm, function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); + return {from: range.from(), to: rightPos }; + }); + }, + undo: function(cm) {cm.undo();}, + redo: function(cm) {cm.redo();}, + undoSelection: function(cm) {cm.undoSelection();}, + redoSelection: function(cm) {cm.redoSelection();}, + goDocStart: function(cm) {cm.extendSelection(Pos(cm.firstLine(), 0));}, + goDocEnd: function(cm) {cm.extendSelection(Pos(cm.lastLine()));}, + goLineStart: function(cm) { + cm.extendSelectionsBy(function(range) { return lineStart(cm, range.head.line); }, + {origin: "+move", bias: 1}); + }, + goLineStartSmart: function(cm) { + cm.extendSelectionsBy(function(range) { + return lineStartSmart(cm, range.head); + }, {origin: "+move", bias: 1}); + }, + goLineEnd: function(cm) { + cm.extendSelectionsBy(function(range) { return lineEnd(cm, range.head.line); }, + {origin: "+move", bias: -1}); + }, + goLineRight: function(cm) { + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); + }, sel_move); + }, + goLineLeft: function(cm) { + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + return cm.coordsChar({left: 0, top: top}, "div"); + }, sel_move); + }, + goLineLeftSmart: function(cm) { + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + var pos = cm.coordsChar({left: 0, top: top}, "div"); + if (pos.ch < cm.getLine(pos.line).search(/\S/)) return lineStartSmart(cm, range.head); + return pos; + }, sel_move); + }, + goLineUp: function(cm) {cm.moveV(-1, "line");}, + goLineDown: function(cm) {cm.moveV(1, "line");}, + goPageUp: function(cm) {cm.moveV(-1, "page");}, + goPageDown: function(cm) {cm.moveV(1, "page");}, + goCharLeft: function(cm) {cm.moveH(-1, "char");}, + goCharRight: function(cm) {cm.moveH(1, "char");}, + goColumnLeft: function(cm) {cm.moveH(-1, "column");}, + goColumnRight: function(cm) {cm.moveH(1, "column");}, + goWordLeft: function(cm) {cm.moveH(-1, "word");}, + goGroupRight: function(cm) {cm.moveH(1, "group");}, + goGroupLeft: function(cm) {cm.moveH(-1, "group");}, + goWordRight: function(cm) {cm.moveH(1, "word");}, + delCharBefore: function(cm) {cm.deleteH(-1, "char");}, + delCharAfter: function(cm) {cm.deleteH(1, "char");}, + delWordBefore: function(cm) {cm.deleteH(-1, "word");}, + delWordAfter: function(cm) {cm.deleteH(1, "word");}, + delGroupBefore: function(cm) {cm.deleteH(-1, "group");}, + delGroupAfter: function(cm) {cm.deleteH(1, "group");}, + indentAuto: function(cm) {cm.indentSelection("smart");}, + indentMore: function(cm) {cm.indentSelection("add");}, + indentLess: function(cm) {cm.indentSelection("subtract");}, + insertTab: function(cm) {cm.replaceSelection("\t");}, + insertSoftTab: function(cm) { + var spaces = [], ranges = cm.listSelections(), tabSize = cm.options.tabSize; + for (var i = 0; i < ranges.length; i++) { + var pos = ranges[i].from(); + var col = countColumn(cm.getLine(pos.line), pos.ch, tabSize); + spaces.push(spaceStr(tabSize - col % tabSize)); + } + cm.replaceSelections(spaces); + }, + defaultTab: function(cm) { + if (cm.somethingSelected()) cm.indentSelection("add"); + else cm.execCommand("insertTab"); + }, + transposeChars: function(cm) { + runInOp(cm, function() { + var ranges = cm.listSelections(), newSel = []; + for (var i = 0; i < ranges.length; i++) { + var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text; + if (line) { + if (cur.ch == line.length) cur = new Pos(cur.line, cur.ch - 1); + if (cur.ch > 0) { + cur = new Pos(cur.line, cur.ch + 1); + cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2), + Pos(cur.line, cur.ch - 2), cur, "+transpose"); + } else if (cur.line > cm.doc.first) { + var prev = getLine(cm.doc, cur.line - 1).text; + if (prev) + cm.replaceRange(line.charAt(0) + cm.doc.lineSeparator() + + prev.charAt(prev.length - 1), + Pos(cur.line - 1, prev.length - 1), Pos(cur.line, 1), "+transpose"); + } + } + newSel.push(new Range(cur, cur)); + } + cm.setSelections(newSel); + }); + }, + newlineAndIndent: function(cm) { + runInOp(cm, function() { + var len = cm.listSelections().length; + for (var i = 0; i < len; i++) { + var range = cm.listSelections()[i]; + cm.replaceRange(cm.doc.lineSeparator(), range.anchor, range.head, "+input"); + cm.indentLine(range.from().line + 1, null, true); + } + ensureCursorVisible(cm); + }); + }, + openLine: function(cm) {cm.replaceSelection("\n", "start")}, + toggleOverwrite: function(cm) {cm.toggleOverwrite();} + }; + + + // STANDARD KEYMAPS + + var keyMap = CodeMirror.keyMap = {}; + + keyMap.basic = { + "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", + "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", + "Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore", + "Tab": "defaultTab", "Shift-Tab": "indentAuto", + "Enter": "newlineAndIndent", "Insert": "toggleOverwrite", + "Esc": "singleSelection" + }; + // Note that the save and find-related commands aren't defined by + // default. User code or addons can define them. Unknown commands + // are simply ignored. + keyMap.pcDefault = { + "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", + "Ctrl-Home": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Up": "goLineUp", "Ctrl-Down": "goLineDown", + "Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", + "Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find", + "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", + "Ctrl-[": "indentLess", "Ctrl-]": "indentMore", + "Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection", + fallthrough: "basic" + }; + // Very basic readline/emacs-style bindings, which are standard on Mac. + keyMap.emacsy = { + "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", + "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", + "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore", + "Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars", + "Ctrl-O": "openLine" + }; + keyMap.macDefault = { + "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", + "Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft", + "Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore", + "Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find", + "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", + "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight", + "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd", + fallthrough: ["basic", "emacsy"] + }; + keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; + + // KEYMAP DISPATCH + + function normalizeKeyName(name) { + var parts = name.split(/-(?!$)/), name = parts[parts.length - 1]; + var alt, ctrl, shift, cmd; + for (var i = 0; i < parts.length - 1; i++) { + var mod = parts[i]; + if (/^(cmd|meta|m)$/i.test(mod)) cmd = true; + else if (/^a(lt)?$/i.test(mod)) alt = true; + else if (/^(c|ctrl|control)$/i.test(mod)) ctrl = true; + else if (/^s(hift)$/i.test(mod)) shift = true; + else throw new Error("Unrecognized modifier name: " + mod); + } + if (alt) name = "Alt-" + name; + if (ctrl) name = "Ctrl-" + name; + if (cmd) name = "Cmd-" + name; + if (shift) name = "Shift-" + name; + return name; + } + + // This is a kludge to keep keymaps mostly working as raw objects + // (backwards compatibility) while at the same time support features + // like normalization and multi-stroke key bindings. It compiles a + // new normalized keymap, and then updates the old object to reflect + // this. + CodeMirror.normalizeKeyMap = function(keymap) { + var copy = {}; + for (var keyname in keymap) if (keymap.hasOwnProperty(keyname)) { + var value = keymap[keyname]; + if (/^(name|fallthrough|(de|at)tach)$/.test(keyname)) continue; + if (value == "...") { delete keymap[keyname]; continue; } + + var keys = map(keyname.split(" "), normalizeKeyName); + for (var i = 0; i < keys.length; i++) { + var val, name; + if (i == keys.length - 1) { + name = keys.join(" "); + val = value; + } else { + name = keys.slice(0, i + 1).join(" "); + val = "..."; + } + var prev = copy[name]; + if (!prev) copy[name] = val; + else if (prev != val) throw new Error("Inconsistent bindings for " + name); + } + delete keymap[keyname]; + } + for (var prop in copy) keymap[prop] = copy[prop]; + return keymap; + }; + + var lookupKey = CodeMirror.lookupKey = function(key, map, handle, context) { + map = getKeyMap(map); + var found = map.call ? map.call(key, context) : map[key]; + if (found === false) return "nothing"; + if (found === "...") return "multi"; + if (found != null && handle(found)) return "handled"; + + if (map.fallthrough) { + if (Object.prototype.toString.call(map.fallthrough) != "[object Array]") + return lookupKey(key, map.fallthrough, handle, context); + for (var i = 0; i < map.fallthrough.length; i++) { + var result = lookupKey(key, map.fallthrough[i], handle, context); + if (result) return result; + } + } + }; + + // Modifier key presses don't count as 'real' key presses for the + // purpose of keymap fallthrough. + var isModifierKey = CodeMirror.isModifierKey = function(value) { + var name = typeof value == "string" ? value : keyNames[value.keyCode]; + return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod"; + }; + + // Look up the name of a key as indicated by an event object. + var keyName = CodeMirror.keyName = function(event, noShift) { + if (presto && event.keyCode == 34 && event["char"]) return false; + var base = keyNames[event.keyCode], name = base; + if (name == null || event.altGraphKey) return false; + if (event.altKey && base != "Alt") name = "Alt-" + name; + if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != "Ctrl") name = "Ctrl-" + name; + if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Cmd") name = "Cmd-" + name; + if (!noShift && event.shiftKey && base != "Shift") name = "Shift-" + name; + return name; + }; + + function getKeyMap(val) { + return typeof val == "string" ? keyMap[val] : val; + } + + // FROMTEXTAREA + + CodeMirror.fromTextArea = function(textarea, options) { + options = options ? copyObj(options) : {}; + options.value = textarea.value; + if (!options.tabindex && textarea.tabIndex) + options.tabindex = textarea.tabIndex; + if (!options.placeholder && textarea.placeholder) + options.placeholder = textarea.placeholder; + // Set autofocus to true if this textarea is focused, or if it has + // autofocus and no other element is focused. + if (options.autofocus == null) { + var hasFocus = activeElt(); + options.autofocus = hasFocus == textarea || + textarea.getAttribute("autofocus") != null && hasFocus == document.body; + } + + function save() {textarea.value = cm.getValue();} + if (textarea.form) { + on(textarea.form, "submit", save); + // Deplorable hack to make the submit method do the right thing. + if (!options.leaveSubmitMethodAlone) { + var form = textarea.form, realSubmit = form.submit; + try { + var wrappedSubmit = form.submit = function() { + save(); + form.submit = realSubmit; + form.submit(); + form.submit = wrappedSubmit; + }; + } catch(e) {} + } + } + + options.finishInit = function(cm) { + cm.save = save; + cm.getTextArea = function() { return textarea; }; + cm.toTextArea = function() { + cm.toTextArea = isNaN; // Prevent this from being ran twice + save(); + textarea.parentNode.removeChild(cm.getWrapperElement()); + textarea.style.display = ""; + if (textarea.form) { + off(textarea.form, "submit", save); + if (typeof textarea.form.submit == "function") + textarea.form.submit = realSubmit; + } + }; + }; + + textarea.style.display = "none"; + var cm = CodeMirror(function(node) { + textarea.parentNode.insertBefore(node, textarea.nextSibling); + }, options); + return cm; + }; + + // STRING STREAM + + // Fed to the mode parsers, provides helper functions to make + // parsers more succinct. + + var StringStream = CodeMirror.StringStream = function(string, tabSize) { + this.pos = this.start = 0; + this.string = string; + this.tabSize = tabSize || 8; + this.lastColumnPos = this.lastColumnValue = 0; + this.lineStart = 0; + }; + + StringStream.prototype = { + eol: function() {return this.pos >= this.string.length;}, + sol: function() {return this.pos == this.lineStart;}, + peek: function() {return this.string.charAt(this.pos) || undefined;}, + next: function() { + if (this.pos < this.string.length) + return this.string.charAt(this.pos++); + }, + eat: function(match) { + var ch = this.string.charAt(this.pos); + if (typeof match == "string") var ok = ch == match; + else var ok = ch && (match.test ? match.test(ch) : match(ch)); + if (ok) {++this.pos; return ch;} + }, + eatWhile: function(match) { + var start = this.pos; + while (this.eat(match)){} + return this.pos > start; + }, + eatSpace: function() { + var start = this.pos; + while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos; + return this.pos > start; + }, + skipToEnd: function() {this.pos = this.string.length;}, + skipTo: function(ch) { + var found = this.string.indexOf(ch, this.pos); + if (found > -1) {this.pos = found; return true;} + }, + backUp: function(n) {this.pos -= n;}, + column: function() { + if (this.lastColumnPos < this.start) { + this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue); + this.lastColumnPos = this.start; + } + return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0); + }, + indentation: function() { + return countColumn(this.string, null, this.tabSize) - + (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0); + }, + match: function(pattern, consume, caseInsensitive) { + if (typeof pattern == "string") { + var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;}; + var substr = this.string.substr(this.pos, pattern.length); + if (cased(substr) == cased(pattern)) { + if (consume !== false) this.pos += pattern.length; + return true; + } + } else { + var match = this.string.slice(this.pos).match(pattern); + if (match && match.index > 0) return null; + if (match && consume !== false) this.pos += match[0].length; + return match; + } + }, + current: function(){return this.string.slice(this.start, this.pos);}, + hideFirstChars: function(n, inner) { + this.lineStart += n; + try { return inner(); } + finally { this.lineStart -= n; } + } + }; + + // TEXTMARKERS + + // Created with markText and setBookmark methods. A TextMarker is a + // handle that can be used to clear or find a marked position in the + // document. Line objects hold arrays (markedSpans) containing + // {from, to, marker} object pointing to such marker objects, and + // indicating that such a marker is present on that line. Multiple + // lines may point to the same marker when it spans across lines. + // The spans will have null for their from/to properties when the + // marker continues beyond the start/end of the line. Markers have + // links back to the lines they currently touch. + + var nextMarkerId = 0; + + var TextMarker = CodeMirror.TextMarker = function(doc, type) { + this.lines = []; + this.type = type; + this.doc = doc; + this.id = ++nextMarkerId; + }; + eventMixin(TextMarker); + + // Clear the marker. + TextMarker.prototype.clear = function() { + if (this.explicitlyCleared) return; + var cm = this.doc.cm, withOp = cm && !cm.curOp; + if (withOp) startOperation(cm); + if (hasHandler(this, "clear")) { + var found = this.find(); + if (found) signalLater(this, "clear", found.from, found.to); + } + var min = null, max = null; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (cm && !this.collapsed) regLineChange(cm, lineNo(line), "text"); + else if (cm) { + if (span.to != null) max = lineNo(line); + if (span.from != null) min = lineNo(line); + } + line.markedSpans = removeMarkedSpan(line.markedSpans, span); + if (span.from == null && this.collapsed && !lineIsHidden(this.doc, line) && cm) + updateLineHeight(line, textHeight(cm.display)); + } + if (cm && this.collapsed && !cm.options.lineWrapping) for (var i = 0; i < this.lines.length; ++i) { + var visual = visualLine(this.lines[i]), len = lineLength(visual); + if (len > cm.display.maxLineLength) { + cm.display.maxLine = visual; + cm.display.maxLineLength = len; + cm.display.maxLineChanged = true; + } + } + + if (min != null && cm && this.collapsed) regChange(cm, min, max + 1); + this.lines.length = 0; + this.explicitlyCleared = true; + if (this.atomic && this.doc.cantEdit) { + this.doc.cantEdit = false; + if (cm) reCheckSelection(cm.doc); + } + if (cm) signalLater(cm, "markerCleared", cm, this); + if (withOp) endOperation(cm); + if (this.parent) this.parent.clear(); + }; + + // Find the position of the marker in the document. Returns a {from, + // to} object by default. Side can be passed to get a specific side + // -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the + // Pos objects returned contain a line object, rather than a line + // number (used to prevent looking up the same line twice). + TextMarker.prototype.find = function(side, lineObj) { + if (side == null && this.type == "bookmark") side = 1; + var from, to; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (span.from != null) { + from = Pos(lineObj ? line : lineNo(line), span.from); + if (side == -1) return from; + } + if (span.to != null) { + to = Pos(lineObj ? line : lineNo(line), span.to); + if (side == 1) return to; + } + } + return from && {from: from, to: to}; + }; + + // Signals that the marker's widget changed, and surrounding layout + // should be recomputed. + TextMarker.prototype.changed = function() { + var pos = this.find(-1, true), widget = this, cm = this.doc.cm; + if (!pos || !cm) return; + runInOp(cm, function() { + var line = pos.line, lineN = lineNo(pos.line); + var view = findViewForLine(cm, lineN); + if (view) { + clearLineMeasurementCacheFor(view); + cm.curOp.selectionChanged = cm.curOp.forceUpdate = true; + } + cm.curOp.updateMaxLine = true; + if (!lineIsHidden(widget.doc, line) && widget.height != null) { + var oldHeight = widget.height; + widget.height = null; + var dHeight = widgetHeight(widget) - oldHeight; + if (dHeight) + updateLineHeight(line, line.height + dHeight); + } + }); + }; + + TextMarker.prototype.attachLine = function(line) { + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1) + (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this); + } + this.lines.push(line); + }; + TextMarker.prototype.detachLine = function(line) { + this.lines.splice(indexOf(this.lines, line), 1); + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + (op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this); + } + }; + + // Collapsed markers have unique ids, in order to be able to order + // them, which is needed for uniquely determining an outer marker + // when they overlap (they may nest, but not partially overlap). + var nextMarkerId = 0; + + // Create a marker, wire it up to the right lines, and + function markText(doc, from, to, options, type) { + // Shared markers (across linked documents) are handled separately + // (markTextShared will call out to this again, once per + // document). + if (options && options.shared) return markTextShared(doc, from, to, options, type); + // Ensure we are in an operation. + if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type); + + var marker = new TextMarker(doc, type), diff = cmp(from, to); + if (options) copyObj(options, marker, false); + // Don't connect empty markers unless clearWhenEmpty is false + if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false) + return marker; + if (marker.replacedWith) { + // Showing up as a widget implies collapsed (widget replaces text) + marker.collapsed = true; + marker.widgetNode = elt("span", [marker.replacedWith], "CodeMirror-widget"); + if (!options.handleMouseEvents) marker.widgetNode.setAttribute("cm-ignore-events", "true"); + if (options.insertLeft) marker.widgetNode.insertLeft = true; + } + if (marker.collapsed) { + if (conflictingCollapsedRange(doc, from.line, from, to, marker) || + from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker)) + throw new Error("Inserting collapsed marker partially overlapping an existing one"); + sawCollapsedSpans = true; + } + + if (marker.addToHistory) + addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, NaN); + + var curLine = from.line, cm = doc.cm, updateMaxLine; + doc.iter(curLine, to.line + 1, function(line) { + if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine) + updateMaxLine = true; + if (marker.collapsed && curLine != from.line) updateLineHeight(line, 0); + addMarkedSpan(line, new MarkedSpan(marker, + curLine == from.line ? from.ch : null, + curLine == to.line ? to.ch : null)); + ++curLine; + }); + // lineIsHidden depends on the presence of the spans, so needs a second pass + if (marker.collapsed) doc.iter(from.line, to.line + 1, function(line) { + if (lineIsHidden(doc, line)) updateLineHeight(line, 0); + }); + + if (marker.clearOnEnter) on(marker, "beforeCursorEnter", function() { marker.clear(); }); + + if (marker.readOnly) { + sawReadOnlySpans = true; + if (doc.history.done.length || doc.history.undone.length) + doc.clearHistory(); + } + if (marker.collapsed) { + marker.id = ++nextMarkerId; + marker.atomic = true; + } + if (cm) { + // Sync editor state + if (updateMaxLine) cm.curOp.updateMaxLine = true; + if (marker.collapsed) + regChange(cm, from.line, to.line + 1); + else if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.css) + for (var i = from.line; i <= to.line; i++) regLineChange(cm, i, "text"); + if (marker.atomic) reCheckSelection(cm.doc); + signalLater(cm, "markerAdded", cm, marker); + } + return marker; + } + + // SHARED TEXTMARKERS + + // A shared marker spans multiple linked documents. It is + // implemented as a meta-marker-object controlling multiple normal + // markers. + var SharedTextMarker = CodeMirror.SharedTextMarker = function(markers, primary) { + this.markers = markers; + this.primary = primary; + for (var i = 0; i < markers.length; ++i) + markers[i].parent = this; + }; + eventMixin(SharedTextMarker); + + SharedTextMarker.prototype.clear = function() { + if (this.explicitlyCleared) return; + this.explicitlyCleared = true; + for (var i = 0; i < this.markers.length; ++i) + this.markers[i].clear(); + signalLater(this, "clear"); + }; + SharedTextMarker.prototype.find = function(side, lineObj) { + return this.primary.find(side, lineObj); + }; + + function markTextShared(doc, from, to, options, type) { + options = copyObj(options); + options.shared = false; + var markers = [markText(doc, from, to, options, type)], primary = markers[0]; + var widget = options.widgetNode; + linkedDocs(doc, function(doc) { + if (widget) options.widgetNode = widget.cloneNode(true); + markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type)); + for (var i = 0; i < doc.linked.length; ++i) + if (doc.linked[i].isParent) return; + primary = lst(markers); + }); + return new SharedTextMarker(markers, primary); + } + + function findSharedMarkers(doc) { + return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())), + function(m) { return m.parent; }); + } + + function copySharedMarkers(doc, markers) { + for (var i = 0; i < markers.length; i++) { + var marker = markers[i], pos = marker.find(); + var mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to); + if (cmp(mFrom, mTo)) { + var subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type); + marker.markers.push(subMark); + subMark.parent = marker; + } + } + } + + function detachSharedMarkers(markers) { + for (var i = 0; i < markers.length; i++) { + var marker = markers[i], linked = [marker.primary.doc];; + linkedDocs(marker.primary.doc, function(d) { linked.push(d); }); + for (var j = 0; j < marker.markers.length; j++) { + var subMarker = marker.markers[j]; + if (indexOf(linked, subMarker.doc) == -1) { + subMarker.parent = null; + marker.markers.splice(j--, 1); + } + } + } + } + + // TEXTMARKER SPANS + + function MarkedSpan(marker, from, to) { + this.marker = marker; + this.from = from; this.to = to; + } + + // Search an array of spans for a span matching the given marker. + function getMarkedSpanFor(spans, marker) { + if (spans) for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.marker == marker) return span; + } + } + // Remove a span from an array, returning undefined if no spans are + // left (we don't store arrays for lines without spans). + function removeMarkedSpan(spans, span) { + for (var r, i = 0; i < spans.length; ++i) + if (spans[i] != span) (r || (r = [])).push(spans[i]); + return r; + } + // Add a span to a line. + function addMarkedSpan(line, span) { + line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]; + span.marker.attachLine(line); + } + + // Used for the algorithm that adjusts markers for a change in the + // document. These functions cut an array of spans at a given + // character position, returning an array of remaining chunks (or + // undefined if nothing remains). + function markedSpansBefore(old, startCh, isInsert) { + if (old) for (var i = 0, nw; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh); + if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) { + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh); + (nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to)); + } + } + return nw; + } + function markedSpansAfter(old, endCh, isInsert) { + if (old) for (var i = 0, nw; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh); + if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) { + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh); + (nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh, + span.to == null ? null : span.to - endCh)); + } + } + return nw; + } + + // Given a change object, compute the new set of marker spans that + // cover the line in which the change took place. Removes spans + // entirely within the change, reconnects spans belonging to the + // same marker that appear on both sides of the change, and cuts off + // spans partially within the change. Returns an array of span + // arrays with one element for each line in (after) the change. + function stretchSpansOverChange(doc, change) { + if (change.full) return null; + var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans; + var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans; + if (!oldFirst && !oldLast) return null; + + var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0; + // Get the spans that 'stick out' on both sides + var first = markedSpansBefore(oldFirst, startCh, isInsert); + var last = markedSpansAfter(oldLast, endCh, isInsert); + + // Next, merge those two ends + var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0); + if (first) { + // Fix up .to properties of first + for (var i = 0; i < first.length; ++i) { + var span = first[i]; + if (span.to == null) { + var found = getMarkedSpanFor(last, span.marker); + if (!found) span.to = startCh; + else if (sameLine) span.to = found.to == null ? null : found.to + offset; + } + } + } + if (last) { + // Fix up .from in last (or move them into first in case of sameLine) + for (var i = 0; i < last.length; ++i) { + var span = last[i]; + if (span.to != null) span.to += offset; + if (span.from == null) { + var found = getMarkedSpanFor(first, span.marker); + if (!found) { + span.from = offset; + if (sameLine) (first || (first = [])).push(span); + } + } else { + span.from += offset; + if (sameLine) (first || (first = [])).push(span); + } + } + } + // Make sure we didn't create any zero-length spans + if (first) first = clearEmptySpans(first); + if (last && last != first) last = clearEmptySpans(last); + + var newMarkers = [first]; + if (!sameLine) { + // Fill gap with whole-line-spans + var gap = change.text.length - 2, gapMarkers; + if (gap > 0 && first) + for (var i = 0; i < first.length; ++i) + if (first[i].to == null) + (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i].marker, null, null)); + for (var i = 0; i < gap; ++i) + newMarkers.push(gapMarkers); + newMarkers.push(last); + } + return newMarkers; + } + + // Remove spans that are empty and don't have a clearWhenEmpty + // option of false. + function clearEmptySpans(spans) { + for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false) + spans.splice(i--, 1); + } + if (!spans.length) return null; + return spans; + } + + // Used for un/re-doing changes from the history. Combines the + // result of computing the existing spans with the set of spans that + // existed in the history (so that deleting around a span and then + // undoing brings back the span). + function mergeOldSpans(doc, change) { + var old = getOldSpans(doc, change); + var stretched = stretchSpansOverChange(doc, change); + if (!old) return stretched; + if (!stretched) return old; + + for (var i = 0; i < old.length; ++i) { + var oldCur = old[i], stretchCur = stretched[i]; + if (oldCur && stretchCur) { + spans: for (var j = 0; j < stretchCur.length; ++j) { + var span = stretchCur[j]; + for (var k = 0; k < oldCur.length; ++k) + if (oldCur[k].marker == span.marker) continue spans; + oldCur.push(span); + } + } else if (stretchCur) { + old[i] = stretchCur; + } + } + return old; + } + + // Used to 'clip' out readOnly ranges when making a change. + function removeReadOnlyRanges(doc, from, to) { + var markers = null; + doc.iter(from.line, to.line + 1, function(line) { + if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) { + var mark = line.markedSpans[i].marker; + if (mark.readOnly && (!markers || indexOf(markers, mark) == -1)) + (markers || (markers = [])).push(mark); + } + }); + if (!markers) return null; + var parts = [{from: from, to: to}]; + for (var i = 0; i < markers.length; ++i) { + var mk = markers[i], m = mk.find(0); + for (var j = 0; j < parts.length; ++j) { + var p = parts[j]; + if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) continue; + var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to); + if (dfrom < 0 || !mk.inclusiveLeft && !dfrom) + newParts.push({from: p.from, to: m.from}); + if (dto > 0 || !mk.inclusiveRight && !dto) + newParts.push({from: m.to, to: p.to}); + parts.splice.apply(parts, newParts); + j += newParts.length - 1; + } + } + return parts; + } + + // Connect or disconnect spans from a line. + function detachMarkedSpans(line) { + var spans = line.markedSpans; + if (!spans) return; + for (var i = 0; i < spans.length; ++i) + spans[i].marker.detachLine(line); + line.markedSpans = null; + } + function attachMarkedSpans(line, spans) { + if (!spans) return; + for (var i = 0; i < spans.length; ++i) + spans[i].marker.attachLine(line); + line.markedSpans = spans; + } + + // Helpers used when computing which overlapping collapsed span + // counts as the larger one. + function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0; } + function extraRight(marker) { return marker.inclusiveRight ? 1 : 0; } + + // Returns a number indicating which of two overlapping collapsed + // spans is larger (and thus includes the other). Falls back to + // comparing ids when the spans cover exactly the same range. + function compareCollapsedMarkers(a, b) { + var lenDiff = a.lines.length - b.lines.length; + if (lenDiff != 0) return lenDiff; + var aPos = a.find(), bPos = b.find(); + var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b); + if (fromCmp) return -fromCmp; + var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b); + if (toCmp) return toCmp; + return b.id - a.id; + } + + // Find out whether a line ends or starts in a collapsed span. If + // so, return the marker for that span. + function collapsedSpanAtSide(line, start) { + var sps = sawCollapsedSpans && line.markedSpans, found; + if (sps) for (var sp, i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (sp.marker.collapsed && (start ? sp.from : sp.to) == null && + (!found || compareCollapsedMarkers(found, sp.marker) < 0)) + found = sp.marker; + } + return found; + } + function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true); } + function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false); } + + // Test whether there exists a collapsed span that partially + // overlaps (covers the start or end, but not both) of a new span. + // Such overlap is not allowed. + function conflictingCollapsedRange(doc, lineNo, from, to, marker) { + var line = getLine(doc, lineNo); + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) for (var i = 0; i < sps.length; ++i) { + var sp = sps[i]; + if (!sp.marker.collapsed) continue; + var found = sp.marker.find(0); + var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker); + var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker); + if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) continue; + if (fromCmp <= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.to, from) >= 0 : cmp(found.to, from) > 0) || + fromCmp >= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.from, to) <= 0 : cmp(found.from, to) < 0)) + return true; + } + } + + // A visual line is a line as drawn on the screen. Folding, for + // example, can cause multiple logical lines to appear on the same + // visual line. This finds the start of the visual line that the + // given line is part of (usually that is the line itself). + function visualLine(line) { + var merged; + while (merged = collapsedSpanAtStart(line)) + line = merged.find(-1, true).line; + return line; + } + + // Returns an array of logical lines that continue the visual line + // started by the argument, or undefined if there are no such lines. + function visualLineContinued(line) { + var merged, lines; + while (merged = collapsedSpanAtEnd(line)) { + line = merged.find(1, true).line; + (lines || (lines = [])).push(line); + } + return lines; + } + + // Get the line number of the start of the visual line that the + // given line number is part of. + function visualLineNo(doc, lineN) { + var line = getLine(doc, lineN), vis = visualLine(line); + if (line == vis) return lineN; + return lineNo(vis); + } + // Get the line number of the start of the next visual line after + // the given line. + function visualLineEndNo(doc, lineN) { + if (lineN > doc.lastLine()) return lineN; + var line = getLine(doc, lineN), merged; + if (!lineIsHidden(doc, line)) return lineN; + while (merged = collapsedSpanAtEnd(line)) + line = merged.find(1, true).line; + return lineNo(line) + 1; + } + + // Compute whether a line is hidden. Lines count as hidden when they + // are part of a visual line that starts with another line, or when + // they are entirely covered by collapsed, non-widget span. + function lineIsHidden(doc, line) { + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) for (var sp, i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (!sp.marker.collapsed) continue; + if (sp.from == null) return true; + if (sp.marker.widgetNode) continue; + if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp)) + return true; + } + } + function lineIsHiddenInner(doc, line, span) { + if (span.to == null) { + var end = span.marker.find(1, true); + return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker)); + } + if (span.marker.inclusiveRight && span.to == line.text.length) + return true; + for (var sp, i = 0; i < line.markedSpans.length; ++i) { + sp = line.markedSpans[i]; + if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to && + (sp.to == null || sp.to != span.from) && + (sp.marker.inclusiveLeft || span.marker.inclusiveRight) && + lineIsHiddenInner(doc, line, sp)) return true; + } + } + + // LINE WIDGETS + + // Line widgets are block elements displayed above or below a line. + + var LineWidget = CodeMirror.LineWidget = function(doc, node, options) { + if (options) for (var opt in options) if (options.hasOwnProperty(opt)) + this[opt] = options[opt]; + this.doc = doc; + this.node = node; + }; + eventMixin(LineWidget); + + function adjustScrollWhenAboveVisible(cm, line, diff) { + if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop)) + addToScrollPos(cm, null, diff); + } + + LineWidget.prototype.clear = function() { + var cm = this.doc.cm, ws = this.line.widgets, line = this.line, no = lineNo(line); + if (no == null || !ws) return; + for (var i = 0; i < ws.length; ++i) if (ws[i] == this) ws.splice(i--, 1); + if (!ws.length) line.widgets = null; + var height = widgetHeight(this); + updateLineHeight(line, Math.max(0, line.height - height)); + if (cm) runInOp(cm, function() { + adjustScrollWhenAboveVisible(cm, line, -height); + regLineChange(cm, no, "widget"); + }); + }; + LineWidget.prototype.changed = function() { + var oldH = this.height, cm = this.doc.cm, line = this.line; + this.height = null; + var diff = widgetHeight(this) - oldH; + if (!diff) return; + updateLineHeight(line, line.height + diff); + if (cm) runInOp(cm, function() { + cm.curOp.forceUpdate = true; + adjustScrollWhenAboveVisible(cm, line, diff); + }); + }; + + function widgetHeight(widget) { + if (widget.height != null) return widget.height; + var cm = widget.doc.cm; + if (!cm) return 0; + if (!contains(document.body, widget.node)) { + var parentStyle = "position: relative;"; + if (widget.coverGutter) + parentStyle += "margin-left: -" + cm.display.gutters.offsetWidth + "px;"; + if (widget.noHScroll) + parentStyle += "width: " + cm.display.wrapper.clientWidth + "px;"; + removeChildrenAndAdd(cm.display.measure, elt("div", [widget.node], null, parentStyle)); + } + return widget.height = widget.node.parentNode.offsetHeight; + } + + function addLineWidget(doc, handle, node, options) { + var widget = new LineWidget(doc, node, options); + var cm = doc.cm; + if (cm && widget.noHScroll) cm.display.alignWidgets = true; + changeLine(doc, handle, "widget", function(line) { + var widgets = line.widgets || (line.widgets = []); + if (widget.insertAt == null) widgets.push(widget); + else widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget); + widget.line = line; + if (cm && !lineIsHidden(doc, line)) { + var aboveVisible = heightAtLine(line) < doc.scrollTop; + updateLineHeight(line, line.height + widgetHeight(widget)); + if (aboveVisible) addToScrollPos(cm, null, widget.height); + cm.curOp.forceUpdate = true; + } + return true; + }); + return widget; + } + + // LINE DATA STRUCTURE + + // Line objects. These hold state related to a line, including + // highlighting info (the styles array). + var Line = CodeMirror.Line = function(text, markedSpans, estimateHeight) { + this.text = text; + attachMarkedSpans(this, markedSpans); + this.height = estimateHeight ? estimateHeight(this) : 1; + }; + eventMixin(Line); + Line.prototype.lineNo = function() { return lineNo(this); }; + + // Change the content (text, markers) of a line. Automatically + // invalidates cached information and tries to re-estimate the + // line's height. + function updateLine(line, text, markedSpans, estimateHeight) { + line.text = text; + if (line.stateAfter) line.stateAfter = null; + if (line.styles) line.styles = null; + if (line.order != null) line.order = null; + detachMarkedSpans(line); + attachMarkedSpans(line, markedSpans); + var estHeight = estimateHeight ? estimateHeight(line) : 1; + if (estHeight != line.height) updateLineHeight(line, estHeight); + } + + // Detach a line from the document tree and its markers. + function cleanUpLine(line) { + line.parent = null; + detachMarkedSpans(line); + } + + function extractLineClasses(type, output) { + if (type) for (;;) { + var lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/); + if (!lineClass) break; + type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length); + var prop = lineClass[1] ? "bgClass" : "textClass"; + if (output[prop] == null) + output[prop] = lineClass[2]; + else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(output[prop])) + output[prop] += " " + lineClass[2]; + } + return type; + } + + function callBlankLine(mode, state) { + if (mode.blankLine) return mode.blankLine(state); + if (!mode.innerMode) return; + var inner = CodeMirror.innerMode(mode, state); + if (inner.mode.blankLine) return inner.mode.blankLine(inner.state); + } + + function readToken(mode, stream, state, inner) { + for (var i = 0; i < 10; i++) { + if (inner) inner[0] = CodeMirror.innerMode(mode, state).mode; + var style = mode.token(stream, state); + if (stream.pos > stream.start) return style; + } + throw new Error("Mode " + mode.name + " failed to advance stream."); + } + + // Utility for getTokenAt and getLineTokens + function takeToken(cm, pos, precise, asArray) { + function getObj(copy) { + return {start: stream.start, end: stream.pos, + string: stream.current(), + type: style || null, + state: copy ? copyState(doc.mode, state) : state}; + } + + var doc = cm.doc, mode = doc.mode, style; + pos = clipPos(doc, pos); + var line = getLine(doc, pos.line), state = getStateBefore(cm, pos.line, precise); + var stream = new StringStream(line.text, cm.options.tabSize), tokens; + if (asArray) tokens = []; + while ((asArray || stream.pos < pos.ch) && !stream.eol()) { + stream.start = stream.pos; + style = readToken(mode, stream, state); + if (asArray) tokens.push(getObj(true)); + } + return asArray ? tokens : getObj(); + } + + // Run the given mode's parser over a line, calling f for each token. + function runMode(cm, text, mode, state, f, lineClasses, forceToEnd) { + var flattenSpans = mode.flattenSpans; + if (flattenSpans == null) flattenSpans = cm.options.flattenSpans; + var curStart = 0, curStyle = null; + var stream = new StringStream(text, cm.options.tabSize), style; + var inner = cm.options.addModeClass && [null]; + if (text == "") extractLineClasses(callBlankLine(mode, state), lineClasses); + while (!stream.eol()) { + if (stream.pos > cm.options.maxHighlightLength) { + flattenSpans = false; + if (forceToEnd) processLine(cm, text, state, stream.pos); + stream.pos = text.length; + style = null; + } else { + style = extractLineClasses(readToken(mode, stream, state, inner), lineClasses); + } + if (inner) { + var mName = inner[0].name; + if (mName) style = "m-" + (style ? mName + " " + style : mName); + } + if (!flattenSpans || curStyle != style) { + while (curStart < stream.start) { + curStart = Math.min(stream.start, curStart + 50000); + f(curStart, curStyle); + } + curStyle = style; + } + stream.start = stream.pos; + } + while (curStart < stream.pos) { + // Webkit seems to refuse to render text nodes longer than 57444 characters + var pos = Math.min(stream.pos, curStart + 50000); + f(pos, curStyle); + curStart = pos; + } + } + + // Compute a style array (an array starting with a mode generation + // -- for invalidation -- followed by pairs of end positions and + // style strings), which is used to highlight the tokens on the + // line. + function highlightLine(cm, line, state, forceToEnd) { + // A styles array always starts with a number identifying the + // mode/overlays that it is based on (for easy invalidation). + var st = [cm.state.modeGen], lineClasses = {}; + // Compute the base array of styles + runMode(cm, line.text, cm.doc.mode, state, function(end, style) { + st.push(end, style); + }, lineClasses, forceToEnd); + + // Run overlays, adjust style array. + for (var o = 0; o < cm.state.overlays.length; ++o) { + var overlay = cm.state.overlays[o], i = 1, at = 0; + runMode(cm, line.text, overlay.mode, true, function(end, style) { + var start = i; + // Ensure there's a token end at the current position, and that i points at it + while (at < end) { + var i_end = st[i]; + if (i_end > end) + st.splice(i, 1, end, st[i+1], i_end); + i += 2; + at = Math.min(end, i_end); + } + if (!style) return; + if (overlay.opaque) { + st.splice(start, i - start, end, "cm-overlay " + style); + i = start + 2; + } else { + for (; start < i; start += 2) { + var cur = st[start+1]; + st[start+1] = (cur ? cur + " " : "") + "cm-overlay " + style; + } + } + }, lineClasses); + } + + return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null}; + } + + function getLineStyles(cm, line, updateFrontier) { + if (!line.styles || line.styles[0] != cm.state.modeGen) { + var state = getStateBefore(cm, lineNo(line)); + var result = highlightLine(cm, line, line.text.length > cm.options.maxHighlightLength ? copyState(cm.doc.mode, state) : state); + line.stateAfter = state; + line.styles = result.styles; + if (result.classes) line.styleClasses = result.classes; + else if (line.styleClasses) line.styleClasses = null; + if (updateFrontier === cm.doc.frontier) cm.doc.frontier++; + } + return line.styles; + } + + // Lightweight form of highlight -- proceed over this line and + // update state, but don't save a style array. Used for lines that + // aren't currently visible. + function processLine(cm, text, state, startAt) { + var mode = cm.doc.mode; + var stream = new StringStream(text, cm.options.tabSize); + stream.start = stream.pos = startAt || 0; + if (text == "") callBlankLine(mode, state); + while (!stream.eol()) { + readToken(mode, stream, state); + stream.start = stream.pos; + } + } + + // Convert a style as returned by a mode (either null, or a string + // containing one or more styles) to a CSS style. This is cached, + // and also looks for line-wide styles. + var styleToClassCache = {}, styleToClassCacheWithMode = {}; + function interpretTokenStyle(style, options) { + if (!style || /^\s*$/.test(style)) return null; + var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache; + return cache[style] || + (cache[style] = style.replace(/\S+/g, "cm-$&")); + } + + // Render the DOM representation of the text of a line. Also builds + // up a 'line map', which points at the DOM nodes that represent + // specific stretches of text, and is used by the measuring code. + // The returned object contains the DOM node, this map, and + // information about line-wide styles that were set by the mode. + function buildLineContent(cm, lineView) { + // The padding-right forces the element to have a 'border', which + // is needed on Webkit to be able to get line-level bounding + // rectangles for it (in measureChar). + var content = elt("span", null, null, webkit ? "padding-right: .1px" : null); + var builder = {pre: elt("pre", [content], "CodeMirror-line"), content: content, + col: 0, pos: 0, cm: cm, + splitSpaces: (ie || webkit) && cm.getOption("lineWrapping")}; + lineView.measure = {}; + + // Iterate over the logical lines that make up this visual line. + for (var i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) { + var line = i ? lineView.rest[i - 1] : lineView.line, order; + builder.pos = 0; + builder.addToken = buildToken; + // Optionally wire in some hacks into the token-rendering + // algorithm, to deal with browser quirks. + if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line))) + builder.addToken = buildTokenBadBidi(builder.addToken, order); + builder.map = []; + var allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line); + insertLineContent(line, builder, getLineStyles(cm, line, allowFrontierUpdate)); + if (line.styleClasses) { + if (line.styleClasses.bgClass) + builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || ""); + if (line.styleClasses.textClass) + builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || ""); + } + + // Ensure at least a single node is present, for measuring. + if (builder.map.length == 0) + builder.map.push(0, 0, builder.content.appendChild(zeroWidthElement(cm.display.measure))); + + // Store the map and a cache object for the current logical line + if (i == 0) { + lineView.measure.map = builder.map; + lineView.measure.cache = {}; + } else { + (lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map); + (lineView.measure.caches || (lineView.measure.caches = [])).push({}); + } + } + + // See issue #2901 + if (webkit) { + var last = builder.content.lastChild + if (/\bcm-tab\b/.test(last.className) || (last.querySelector && last.querySelector(".cm-tab"))) + builder.content.className = "cm-tab-wrap-hack"; + } + + signal(cm, "renderLine", cm, lineView.line, builder.pre); + if (builder.pre.className) + builder.textClass = joinClasses(builder.pre.className, builder.textClass || ""); + + return builder; + } + + function defaultSpecialCharPlaceholder(ch) { + var token = elt("span", "\u2022", "cm-invalidchar"); + token.title = "\\u" + ch.charCodeAt(0).toString(16); + token.setAttribute("aria-label", token.title); + return token; + } + + // Build up the DOM representation for a single token, and add it to + // the line map. Takes care to render special characters separately. + function buildToken(builder, text, style, startStyle, endStyle, title, css) { + if (!text) return; + var displayText = builder.splitSpaces ? text.replace(/ {3,}/g, splitSpaces) : text; + var special = builder.cm.state.specialChars, mustWrap = false; + if (!special.test(text)) { + builder.col += text.length; + var content = document.createTextNode(displayText); + builder.map.push(builder.pos, builder.pos + text.length, content); + if (ie && ie_version < 9) mustWrap = true; + builder.pos += text.length; + } else { + var content = document.createDocumentFragment(), pos = 0; + while (true) { + special.lastIndex = pos; + var m = special.exec(text); + var skipped = m ? m.index - pos : text.length - pos; + if (skipped) { + var txt = document.createTextNode(displayText.slice(pos, pos + skipped)); + if (ie && ie_version < 9) content.appendChild(elt("span", [txt])); + else content.appendChild(txt); + builder.map.push(builder.pos, builder.pos + skipped, txt); + builder.col += skipped; + builder.pos += skipped; + } + if (!m) break; + pos += skipped + 1; + if (m[0] == "\t") { + var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize; + var txt = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); + txt.setAttribute("role", "presentation"); + txt.setAttribute("cm-text", "\t"); + builder.col += tabWidth; + } else if (m[0] == "\r" || m[0] == "\n") { + var txt = content.appendChild(elt("span", m[0] == "\r" ? "\u240d" : "\u2424", "cm-invalidchar")); + txt.setAttribute("cm-text", m[0]); + builder.col += 1; + } else { + var txt = builder.cm.options.specialCharPlaceholder(m[0]); + txt.setAttribute("cm-text", m[0]); + if (ie && ie_version < 9) content.appendChild(elt("span", [txt])); + else content.appendChild(txt); + builder.col += 1; + } + builder.map.push(builder.pos, builder.pos + 1, txt); + builder.pos++; + } + } + if (style || startStyle || endStyle || mustWrap || css) { + var fullStyle = style || ""; + if (startStyle) fullStyle += startStyle; + if (endStyle) fullStyle += endStyle; + var token = elt("span", [content], fullStyle, css); + if (title) token.title = title; + return builder.content.appendChild(token); + } + builder.content.appendChild(content); + } + + function splitSpaces(old) { + var out = " "; + for (var i = 0; i < old.length - 2; ++i) out += i % 2 ? " " : "\u00a0"; + out += " "; + return out; + } + + // Work around nonsense dimensions being reported for stretches of + // right-to-left text. + function buildTokenBadBidi(inner, order) { + return function(builder, text, style, startStyle, endStyle, title, css) { + style = style ? style + " cm-force-border" : "cm-force-border"; + var start = builder.pos, end = start + text.length; + for (;;) { + // Find the part that overlaps with the start of this text + for (var i = 0; i < order.length; i++) { + var part = order[i]; + if (part.to > start && part.from <= start) break; + } + if (part.to >= end) return inner(builder, text, style, startStyle, endStyle, title, css); + inner(builder, text.slice(0, part.to - start), style, startStyle, null, title, css); + startStyle = null; + text = text.slice(part.to - start); + start = part.to; + } + }; + } + + function buildCollapsedSpan(builder, size, marker, ignoreWidget) { + var widget = !ignoreWidget && marker.widgetNode; + if (widget) builder.map.push(builder.pos, builder.pos + size, widget); + if (!ignoreWidget && builder.cm.display.input.needsContentAttribute) { + if (!widget) + widget = builder.content.appendChild(document.createElement("span")); + widget.setAttribute("cm-marker", marker.id); + } + if (widget) { + builder.cm.display.input.setUneditable(widget); + builder.content.appendChild(widget); + } + builder.pos += size; + } + + // Outputs a number of spans to make up a line, taking highlighting + // and marked text into account. + function insertLineContent(line, builder, styles) { + var spans = line.markedSpans, allText = line.text, at = 0; + if (!spans) { + for (var i = 1; i < styles.length; i+=2) + builder.addToken(builder, allText.slice(at, at = styles[i]), interpretTokenStyle(styles[i+1], builder.cm.options)); + return; + } + + var len = allText.length, pos = 0, i = 1, text = "", style, css; + var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed; + for (;;) { + if (nextChange == pos) { // Update current marker set + spanStyle = spanEndStyle = spanStartStyle = title = css = ""; + collapsed = null; nextChange = Infinity; + var foundBookmarks = [], endStyles + for (var j = 0; j < spans.length; ++j) { + var sp = spans[j], m = sp.marker; + if (m.type == "bookmark" && sp.from == pos && m.widgetNode) { + foundBookmarks.push(m); + } else if (sp.from <= pos && (sp.to == null || sp.to > pos || m.collapsed && sp.to == pos && sp.from == pos)) { + if (sp.to != null && sp.to != pos && nextChange > sp.to) { + nextChange = sp.to; + spanEndStyle = ""; + } + if (m.className) spanStyle += " " + m.className; + if (m.css) css = (css ? css + ";" : "") + m.css; + if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle; + if (m.endStyle && sp.to == nextChange) (endStyles || (endStyles = [])).push(m.endStyle, sp.to) + if (m.title && !title) title = m.title; + if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0)) + collapsed = sp; + } else if (sp.from > pos && nextChange > sp.from) { + nextChange = sp.from; + } + } + if (endStyles) for (var j = 0; j < endStyles.length; j += 2) + if (endStyles[j + 1] == nextChange) spanEndStyle += " " + endStyles[j] + + if (!collapsed || collapsed.from == pos) for (var j = 0; j < foundBookmarks.length; ++j) + buildCollapsedSpan(builder, 0, foundBookmarks[j]); + if (collapsed && (collapsed.from || 0) == pos) { + buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos, + collapsed.marker, collapsed.from == null); + if (collapsed.to == null) return; + if (collapsed.to == pos) collapsed = false; + } + } + if (pos >= len) break; + + var upto = Math.min(len, nextChange); + while (true) { + if (text) { + var end = pos + text.length; + if (!collapsed) { + var tokenText = end > upto ? text.slice(0, upto - pos) : text; + builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle, + spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", title, css); + } + if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;} + pos = end; + spanStartStyle = ""; + } + text = allText.slice(at, at = styles[i++]); + style = interpretTokenStyle(styles[i++], builder.cm.options); + } + } + } + + // DOCUMENT DATA STRUCTURE + + // By default, updates that start and end at the beginning of a line + // are treated specially, in order to make the association of line + // widgets and marker elements with the text behave more intuitive. + function isWholeLineUpdate(doc, change) { + return change.from.ch == 0 && change.to.ch == 0 && lst(change.text) == "" && + (!doc.cm || doc.cm.options.wholeLineUpdateBefore); + } + + // Perform a change on the document data structure. + function updateDoc(doc, change, markedSpans, estimateHeight) { + function spansFor(n) {return markedSpans ? markedSpans[n] : null;} + function update(line, text, spans) { + updateLine(line, text, spans, estimateHeight); + signalLater(line, "change", line, change); + } + function linesFor(start, end) { + for (var i = start, result = []; i < end; ++i) + result.push(new Line(text[i], spansFor(i), estimateHeight)); + return result; + } + + var from = change.from, to = change.to, text = change.text; + var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line); + var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line; + + // Adjust the line structure + if (change.full) { + doc.insert(0, linesFor(0, text.length)); + doc.remove(text.length, doc.size - text.length); + } else if (isWholeLineUpdate(doc, change)) { + // This is a whole-line replace. Treated specially to make + // sure line objects move the way they are supposed to. + var added = linesFor(0, text.length - 1); + update(lastLine, lastLine.text, lastSpans); + if (nlines) doc.remove(from.line, nlines); + if (added.length) doc.insert(from.line, added); + } else if (firstLine == lastLine) { + if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans); + } else { + var added = linesFor(1, text.length - 1); + added.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight)); + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + doc.insert(from.line + 1, added); + } + } else if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0)); + doc.remove(from.line + 1, nlines); + } else { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans); + var added = linesFor(1, text.length - 1); + if (nlines > 1) doc.remove(from.line + 1, nlines - 1); + doc.insert(from.line + 1, added); + } + + signalLater(doc, "change", doc, change); + } + + // The document is represented as a BTree consisting of leaves, with + // chunk of lines in them, and branches, with up to ten leaves or + // other branch nodes below them. The top node is always a branch + // node, and is the document object itself (meaning it has + // additional methods and properties). + // + // All nodes have parent links. The tree is used both to go from + // line numbers to line objects, and to go from objects to numbers. + // It also indexes by height, and is used to convert between height + // and line object, and to find the total height of the document. + // + // See also http://marijnhaverbeke.nl/blog/codemirror-line-tree.html + + function LeafChunk(lines) { + this.lines = lines; + this.parent = null; + for (var i = 0, height = 0; i < lines.length; ++i) { + lines[i].parent = this; + height += lines[i].height; + } + this.height = height; + } + + LeafChunk.prototype = { + chunkSize: function() { return this.lines.length; }, + // Remove the n lines at offset 'at'. + removeInner: function(at, n) { + for (var i = at, e = at + n; i < e; ++i) { + var line = this.lines[i]; + this.height -= line.height; + cleanUpLine(line); + signalLater(line, "delete"); + } + this.lines.splice(at, n); + }, + // Helper used to collapse a small branch into a single leaf. + collapse: function(lines) { + lines.push.apply(lines, this.lines); + }, + // Insert the given array of lines at offset 'at', count them as + // having the given height. + insertInner: function(at, lines, height) { + this.height += height; + this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at)); + for (var i = 0; i < lines.length; ++i) lines[i].parent = this; + }, + // Used to iterate over a part of the tree. + iterN: function(at, n, op) { + for (var e = at + n; at < e; ++at) + if (op(this.lines[at])) return true; + } + }; + + function BranchChunk(children) { + this.children = children; + var size = 0, height = 0; + for (var i = 0; i < children.length; ++i) { + var ch = children[i]; + size += ch.chunkSize(); height += ch.height; + ch.parent = this; + } + this.size = size; + this.height = height; + this.parent = null; + } + + BranchChunk.prototype = { + chunkSize: function() { return this.size; }, + removeInner: function(at, n) { + this.size -= n; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var rm = Math.min(n, sz - at), oldHeight = child.height; + child.removeInner(at, rm); + this.height -= oldHeight - child.height; + if (sz == rm) { this.children.splice(i--, 1); child.parent = null; } + if ((n -= rm) == 0) break; + at = 0; + } else at -= sz; + } + // If the result is smaller than 25 lines, ensure that it is a + // single leaf node. + if (this.size - n < 25 && + (this.children.length > 1 || !(this.children[0] instanceof LeafChunk))) { + var lines = []; + this.collapse(lines); + this.children = [new LeafChunk(lines)]; + this.children[0].parent = this; + } + }, + collapse: function(lines) { + for (var i = 0; i < this.children.length; ++i) this.children[i].collapse(lines); + }, + insertInner: function(at, lines, height) { + this.size += lines.length; + this.height += height; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at <= sz) { + child.insertInner(at, lines, height); + if (child.lines && child.lines.length > 50) { + // To avoid memory thrashing when child.lines is huge (e.g. first view of a large file), it's never spliced. + // Instead, small slices are taken. They're taken in order because sequential memory accesses are fastest. + var remaining = child.lines.length % 25 + 25 + for (var pos = remaining; pos < child.lines.length;) { + var leaf = new LeafChunk(child.lines.slice(pos, pos += 25)); + child.height -= leaf.height; + this.children.splice(++i, 0, leaf); + leaf.parent = this; + } + child.lines = child.lines.slice(0, remaining); + this.maybeSpill(); + } + break; + } + at -= sz; + } + }, + // When a node has grown, check whether it should be split. + maybeSpill: function() { + if (this.children.length <= 10) return; + var me = this; + do { + var spilled = me.children.splice(me.children.length - 5, 5); + var sibling = new BranchChunk(spilled); + if (!me.parent) { // Become the parent node + var copy = new BranchChunk(me.children); + copy.parent = me; + me.children = [copy, sibling]; + me = copy; + } else { + me.size -= sibling.size; + me.height -= sibling.height; + var myIndex = indexOf(me.parent.children, me); + me.parent.children.splice(myIndex + 1, 0, sibling); + } + sibling.parent = me.parent; + } while (me.children.length > 10); + me.parent.maybeSpill(); + }, + iterN: function(at, n, op) { + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var used = Math.min(n, sz - at); + if (child.iterN(at, used, op)) return true; + if ((n -= used) == 0) break; + at = 0; + } else at -= sz; + } + } + }; + + var nextDocId = 0; + var Doc = CodeMirror.Doc = function(text, mode, firstLine, lineSep) { + if (!(this instanceof Doc)) return new Doc(text, mode, firstLine, lineSep); + if (firstLine == null) firstLine = 0; + + BranchChunk.call(this, [new LeafChunk([new Line("", null)])]); + this.first = firstLine; + this.scrollTop = this.scrollLeft = 0; + this.cantEdit = false; + this.cleanGeneration = 1; + this.frontier = firstLine; + var start = Pos(firstLine, 0); + this.sel = simpleSelection(start); + this.history = new History(null); + this.id = ++nextDocId; + this.modeOption = mode; + this.lineSep = lineSep; + this.extend = false; + + if (typeof text == "string") text = this.splitLines(text); + updateDoc(this, {from: start, to: start, text: text}); + setSelection(this, simpleSelection(start), sel_dontScroll); + }; + + Doc.prototype = createObj(BranchChunk.prototype, { + constructor: Doc, + // Iterate over the document. Supports two forms -- with only one + // argument, it calls that for each line in the document. With + // three, it iterates over the range given by the first two (with + // the second being non-inclusive). + iter: function(from, to, op) { + if (op) this.iterN(from - this.first, to - from, op); + else this.iterN(this.first, this.first + this.size, from); + }, + + // Non-public interface for adding and removing lines. + insert: function(at, lines) { + var height = 0; + for (var i = 0; i < lines.length; ++i) height += lines[i].height; + this.insertInner(at - this.first, lines, height); + }, + remove: function(at, n) { this.removeInner(at - this.first, n); }, + + // From here, the methods are part of the public interface. Most + // are also available from CodeMirror (editor) instances. + + getValue: function(lineSep) { + var lines = getLines(this, this.first, this.first + this.size); + if (lineSep === false) return lines; + return lines.join(lineSep || this.lineSeparator()); + }, + setValue: docMethodOp(function(code) { + var top = Pos(this.first, 0), last = this.first + this.size - 1; + makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), + text: this.splitLines(code), origin: "setValue", full: true}, true); + setSelection(this, simpleSelection(top)); + }), + replaceRange: function(code, from, to, origin) { + from = clipPos(this, from); + to = to ? clipPos(this, to) : from; + replaceRange(this, code, from, to, origin); + }, + getRange: function(from, to, lineSep) { + var lines = getBetween(this, clipPos(this, from), clipPos(this, to)); + if (lineSep === false) return lines; + return lines.join(lineSep || this.lineSeparator()); + }, + + getLine: function(line) {var l = this.getLineHandle(line); return l && l.text;}, + + getLineHandle: function(line) {if (isLine(this, line)) return getLine(this, line);}, + getLineNumber: function(line) {return lineNo(line);}, + + getLineHandleVisualStart: function(line) { + if (typeof line == "number") line = getLine(this, line); + return visualLine(line); + }, + + lineCount: function() {return this.size;}, + firstLine: function() {return this.first;}, + lastLine: function() {return this.first + this.size - 1;}, + + clipPos: function(pos) {return clipPos(this, pos);}, + + getCursor: function(start) { + var range = this.sel.primary(), pos; + if (start == null || start == "head") pos = range.head; + else if (start == "anchor") pos = range.anchor; + else if (start == "end" || start == "to" || start === false) pos = range.to(); + else pos = range.from(); + return pos; + }, + listSelections: function() { return this.sel.ranges; }, + somethingSelected: function() {return this.sel.somethingSelected();}, + + setCursor: docMethodOp(function(line, ch, options) { + setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options); + }), + setSelection: docMethodOp(function(anchor, head, options) { + setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options); + }), + extendSelection: docMethodOp(function(head, other, options) { + extendSelection(this, clipPos(this, head), other && clipPos(this, other), options); + }), + extendSelections: docMethodOp(function(heads, options) { + extendSelections(this, clipPosArray(this, heads), options); + }), + extendSelectionsBy: docMethodOp(function(f, options) { + var heads = map(this.sel.ranges, f); + extendSelections(this, clipPosArray(this, heads), options); + }), + setSelections: docMethodOp(function(ranges, primary, options) { + if (!ranges.length) return; + for (var i = 0, out = []; i < ranges.length; i++) + out[i] = new Range(clipPos(this, ranges[i].anchor), + clipPos(this, ranges[i].head)); + if (primary == null) primary = Math.min(ranges.length - 1, this.sel.primIndex); + setSelection(this, normalizeSelection(out, primary), options); + }), + addSelection: docMethodOp(function(anchor, head, options) { + var ranges = this.sel.ranges.slice(0); + ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor))); + setSelection(this, normalizeSelection(ranges, ranges.length - 1), options); + }), + + getSelection: function(lineSep) { + var ranges = this.sel.ranges, lines; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this, ranges[i].from(), ranges[i].to()); + lines = lines ? lines.concat(sel) : sel; + } + if (lineSep === false) return lines; + else return lines.join(lineSep || this.lineSeparator()); + }, + getSelections: function(lineSep) { + var parts = [], ranges = this.sel.ranges; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this, ranges[i].from(), ranges[i].to()); + if (lineSep !== false) sel = sel.join(lineSep || this.lineSeparator()); + parts[i] = sel; + } + return parts; + }, + replaceSelection: function(code, collapse, origin) { + var dup = []; + for (var i = 0; i < this.sel.ranges.length; i++) + dup[i] = code; + this.replaceSelections(dup, collapse, origin || "+input"); + }, + replaceSelections: docMethodOp(function(code, collapse, origin) { + var changes = [], sel = this.sel; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + changes[i] = {from: range.from(), to: range.to(), text: this.splitLines(code[i]), origin: origin}; + } + var newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse); + for (var i = changes.length - 1; i >= 0; i--) + makeChange(this, changes[i]); + if (newSel) setSelectionReplaceHistory(this, newSel); + else if (this.cm) ensureCursorVisible(this.cm); + }), + undo: docMethodOp(function() {makeChangeFromHistory(this, "undo");}), + redo: docMethodOp(function() {makeChangeFromHistory(this, "redo");}), + undoSelection: docMethodOp(function() {makeChangeFromHistory(this, "undo", true);}), + redoSelection: docMethodOp(function() {makeChangeFromHistory(this, "redo", true);}), + + setExtending: function(val) {this.extend = val;}, + getExtending: function() {return this.extend;}, + + historySize: function() { + var hist = this.history, done = 0, undone = 0; + for (var i = 0; i < hist.done.length; i++) if (!hist.done[i].ranges) ++done; + for (var i = 0; i < hist.undone.length; i++) if (!hist.undone[i].ranges) ++undone; + return {undo: done, redo: undone}; + }, + clearHistory: function() {this.history = new History(this.history.maxGeneration);}, + + markClean: function() { + this.cleanGeneration = this.changeGeneration(true); + }, + changeGeneration: function(forceSplit) { + if (forceSplit) + this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null; + return this.history.generation; + }, + isClean: function (gen) { + return this.history.generation == (gen || this.cleanGeneration); + }, + + getHistory: function() { + return {done: copyHistoryArray(this.history.done), + undone: copyHistoryArray(this.history.undone)}; + }, + setHistory: function(histData) { + var hist = this.history = new History(this.history.maxGeneration); + hist.done = copyHistoryArray(histData.done.slice(0), null, true); + hist.undone = copyHistoryArray(histData.undone.slice(0), null, true); + }, + + addLineClass: docMethodOp(function(handle, where, cls) { + return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) { + var prop = where == "text" ? "textClass" + : where == "background" ? "bgClass" + : where == "gutter" ? "gutterClass" : "wrapClass"; + if (!line[prop]) line[prop] = cls; + else if (classTest(cls).test(line[prop])) return false; + else line[prop] += " " + cls; + return true; + }); + }), + removeLineClass: docMethodOp(function(handle, where, cls) { + return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) { + var prop = where == "text" ? "textClass" + : where == "background" ? "bgClass" + : where == "gutter" ? "gutterClass" : "wrapClass"; + var cur = line[prop]; + if (!cur) return false; + else if (cls == null) line[prop] = null; + else { + var found = cur.match(classTest(cls)); + if (!found) return false; + var end = found.index + found[0].length; + line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null; + } + return true; + }); + }), + + addLineWidget: docMethodOp(function(handle, node, options) { + return addLineWidget(this, handle, node, options); + }), + removeLineWidget: function(widget) { widget.clear(); }, + + markText: function(from, to, options) { + return markText(this, clipPos(this, from), clipPos(this, to), options, options && options.type || "range"); + }, + setBookmark: function(pos, options) { + var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), + insertLeft: options && options.insertLeft, + clearWhenEmpty: false, shared: options && options.shared, + handleMouseEvents: options && options.handleMouseEvents}; + pos = clipPos(this, pos); + return markText(this, pos, pos, realOpts, "bookmark"); + }, + findMarksAt: function(pos) { + pos = clipPos(this, pos); + var markers = [], spans = getLine(this, pos.line).markedSpans; + if (spans) for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if ((span.from == null || span.from <= pos.ch) && + (span.to == null || span.to >= pos.ch)) + markers.push(span.marker.parent || span.marker); + } + return markers; + }, + findMarks: function(from, to, filter) { + from = clipPos(this, from); to = clipPos(this, to); + var found = [], lineNo = from.line; + this.iter(from.line, to.line + 1, function(line) { + var spans = line.markedSpans; + if (spans) for (var i = 0; i < spans.length; i++) { + var span = spans[i]; + if (!(span.to != null && lineNo == from.line && from.ch >= span.to || + span.from == null && lineNo != from.line || + span.from != null && lineNo == to.line && span.from >= to.ch) && + (!filter || filter(span.marker))) + found.push(span.marker.parent || span.marker); + } + ++lineNo; + }); + return found; + }, + getAllMarks: function() { + var markers = []; + this.iter(function(line) { + var sps = line.markedSpans; + if (sps) for (var i = 0; i < sps.length; ++i) + if (sps[i].from != null) markers.push(sps[i].marker); + }); + return markers; + }, + + posFromIndex: function(off) { + var ch, lineNo = this.first, sepSize = this.lineSeparator().length; + this.iter(function(line) { + var sz = line.text.length + sepSize; + if (sz > off) { ch = off; return true; } + off -= sz; + ++lineNo; + }); + return clipPos(this, Pos(lineNo, ch)); + }, + indexFromPos: function (coords) { + coords = clipPos(this, coords); + var index = coords.ch; + if (coords.line < this.first || coords.ch < 0) return 0; + var sepSize = this.lineSeparator().length; + this.iter(this.first, coords.line, function (line) { + index += line.text.length + sepSize; + }); + return index; + }, + + copy: function(copyHistory) { + var doc = new Doc(getLines(this, this.first, this.first + this.size), + this.modeOption, this.first, this.lineSep); + doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft; + doc.sel = this.sel; + doc.extend = false; + if (copyHistory) { + doc.history.undoDepth = this.history.undoDepth; + doc.setHistory(this.getHistory()); + } + return doc; + }, + + linkedDoc: function(options) { + if (!options) options = {}; + var from = this.first, to = this.first + this.size; + if (options.from != null && options.from > from) from = options.from; + if (options.to != null && options.to < to) to = options.to; + var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from, this.lineSep); + if (options.sharedHist) copy.history = this.history; + (this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}); + copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}]; + copySharedMarkers(copy, findSharedMarkers(this)); + return copy; + }, + unlinkDoc: function(other) { + if (other instanceof CodeMirror) other = other.doc; + if (this.linked) for (var i = 0; i < this.linked.length; ++i) { + var link = this.linked[i]; + if (link.doc != other) continue; + this.linked.splice(i, 1); + other.unlinkDoc(this); + detachSharedMarkers(findSharedMarkers(this)); + break; + } + // If the histories were shared, split them again + if (other.history == this.history) { + var splitIds = [other.id]; + linkedDocs(other, function(doc) {splitIds.push(doc.id);}, true); + other.history = new History(null); + other.history.done = copyHistoryArray(this.history.done, splitIds); + other.history.undone = copyHistoryArray(this.history.undone, splitIds); + } + }, + iterLinkedDocs: function(f) {linkedDocs(this, f);}, + + getMode: function() {return this.mode;}, + getEditor: function() {return this.cm;}, + + splitLines: function(str) { + if (this.lineSep) return str.split(this.lineSep); + return splitLinesAuto(str); + }, + lineSeparator: function() { return this.lineSep || "\n"; } + }); + + // Public alias. + Doc.prototype.eachLine = Doc.prototype.iter; + + // Set up methods on CodeMirror's prototype to redirect to the editor's document. + var dontDelegate = "iter insert remove copy getEditor constructor".split(" "); + for (var prop in Doc.prototype) if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0) + CodeMirror.prototype[prop] = (function(method) { + return function() {return method.apply(this.doc, arguments);}; + })(Doc.prototype[prop]); + + eventMixin(Doc); + + // Call f for all linked documents. + function linkedDocs(doc, f, sharedHistOnly) { + function propagate(doc, skip, sharedHist) { + if (doc.linked) for (var i = 0; i < doc.linked.length; ++i) { + var rel = doc.linked[i]; + if (rel.doc == skip) continue; + var shared = sharedHist && rel.sharedHist; + if (sharedHistOnly && !shared) continue; + f(rel.doc, shared); + propagate(rel.doc, doc, shared); + } + } + propagate(doc, null, true); + } + + // Attach a document to an editor. + function attachDoc(cm, doc) { + if (doc.cm) throw new Error("This document is already in use."); + cm.doc = doc; + doc.cm = cm; + estimateLineHeights(cm); + loadMode(cm); + if (!cm.options.lineWrapping) findMaxLine(cm); + cm.options.mode = doc.modeOption; + regChange(cm); + } + + // LINE UTILITIES + + // Find the line object corresponding to the given line number. + function getLine(doc, n) { + n -= doc.first; + if (n < 0 || n >= doc.size) throw new Error("There is no line " + (n + doc.first) + " in the document."); + for (var chunk = doc; !chunk.lines;) { + for (var i = 0;; ++i) { + var child = chunk.children[i], sz = child.chunkSize(); + if (n < sz) { chunk = child; break; } + n -= sz; + } + } + return chunk.lines[n]; + } + + // Get the part of a document between two positions, as an array of + // strings. + function getBetween(doc, start, end) { + var out = [], n = start.line; + doc.iter(start.line, end.line + 1, function(line) { + var text = line.text; + if (n == end.line) text = text.slice(0, end.ch); + if (n == start.line) text = text.slice(start.ch); + out.push(text); + ++n; + }); + return out; + } + // Get the lines between from and to, as array of strings. + function getLines(doc, from, to) { + var out = []; + doc.iter(from, to, function(line) { out.push(line.text); }); + return out; + } + + // Update the height of a line, propagating the height change + // upwards to parent nodes. + function updateLineHeight(line, height) { + var diff = height - line.height; + if (diff) for (var n = line; n; n = n.parent) n.height += diff; + } + + // Given a line object, find its line number by walking up through + // its parent links. + function lineNo(line) { + if (line.parent == null) return null; + var cur = line.parent, no = indexOf(cur.lines, line); + for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) { + for (var i = 0;; ++i) { + if (chunk.children[i] == cur) break; + no += chunk.children[i].chunkSize(); + } + } + return no + cur.first; + } + + // Find the line at the given vertical position, using the height + // information in the document tree. + function lineAtHeight(chunk, h) { + var n = chunk.first; + outer: do { + for (var i = 0; i < chunk.children.length; ++i) { + var child = chunk.children[i], ch = child.height; + if (h < ch) { chunk = child; continue outer; } + h -= ch; + n += child.chunkSize(); + } + return n; + } while (!chunk.lines); + for (var i = 0; i < chunk.lines.length; ++i) { + var line = chunk.lines[i], lh = line.height; + if (h < lh) break; + h -= lh; + } + return n + i; + } + + + // Find the height above the given line. + function heightAtLine(lineObj) { + lineObj = visualLine(lineObj); + + var h = 0, chunk = lineObj.parent; + for (var i = 0; i < chunk.lines.length; ++i) { + var line = chunk.lines[i]; + if (line == lineObj) break; + else h += line.height; + } + for (var p = chunk.parent; p; chunk = p, p = chunk.parent) { + for (var i = 0; i < p.children.length; ++i) { + var cur = p.children[i]; + if (cur == chunk) break; + else h += cur.height; + } + } + return h; + } + + // Get the bidi ordering for the given line (and cache it). Returns + // false for lines that are fully left-to-right, and an array of + // BidiSpan objects otherwise. + function getOrder(line) { + var order = line.order; + if (order == null) order = line.order = bidiOrdering(line.text); + return order; + } + + // HISTORY + + function History(startGen) { + // Arrays of change events and selections. Doing something adds an + // event to done and clears undo. Undoing moves events from done + // to undone, redoing moves them in the other direction. + this.done = []; this.undone = []; + this.undoDepth = Infinity; + // Used to track when changes can be merged into a single undo + // event + this.lastModTime = this.lastSelTime = 0; + this.lastOp = this.lastSelOp = null; + this.lastOrigin = this.lastSelOrigin = null; + // Used by the isClean() method + this.generation = this.maxGeneration = startGen || 1; + } + + // Create a history change event from an updateDoc-style change + // object. + function historyChangeFromChange(doc, change) { + var histChange = {from: copyPos(change.from), to: changeEnd(change), text: getBetween(doc, change.from, change.to)}; + attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); + linkedDocs(doc, function(doc) {attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);}, true); + return histChange; + } + + // Pop all selection events off the end of a history array. Stop at + // a change event. + function clearSelectionEvents(array) { + while (array.length) { + var last = lst(array); + if (last.ranges) array.pop(); + else break; + } + } + + // Find the top change event in the history. Pop off selection + // events that are in the way. + function lastChangeEvent(hist, force) { + if (force) { + clearSelectionEvents(hist.done); + return lst(hist.done); + } else if (hist.done.length && !lst(hist.done).ranges) { + return lst(hist.done); + } else if (hist.done.length > 1 && !hist.done[hist.done.length - 2].ranges) { + hist.done.pop(); + return lst(hist.done); + } + } + + // Register a change in the history. Merges changes that are within + // a single operation, ore are close together with an origin that + // allows merging (starting with "+") into a single event. + function addChangeToHistory(doc, change, selAfter, opId) { + var hist = doc.history; + hist.undone.length = 0; + var time = +new Date, cur; + + if ((hist.lastOp == opId || + hist.lastOrigin == change.origin && change.origin && + ((change.origin.charAt(0) == "+" && doc.cm && hist.lastModTime > time - doc.cm.options.historyEventDelay) || + change.origin.charAt(0) == "*")) && + (cur = lastChangeEvent(hist, hist.lastOp == opId))) { + // Merge this change into the last event + var last = lst(cur.changes); + if (cmp(change.from, change.to) == 0 && cmp(change.from, last.to) == 0) { + // Optimized case for simple insertion -- don't want to add + // new changesets for every character typed + last.to = changeEnd(change); + } else { + // Add new sub-event + cur.changes.push(historyChangeFromChange(doc, change)); + } + } else { + // Can not be merged, start a new event. + var before = lst(hist.done); + if (!before || !before.ranges) + pushSelectionToHistory(doc.sel, hist.done); + cur = {changes: [historyChangeFromChange(doc, change)], + generation: hist.generation}; + hist.done.push(cur); + while (hist.done.length > hist.undoDepth) { + hist.done.shift(); + if (!hist.done[0].ranges) hist.done.shift(); + } + } + hist.done.push(selAfter); + hist.generation = ++hist.maxGeneration; + hist.lastModTime = hist.lastSelTime = time; + hist.lastOp = hist.lastSelOp = opId; + hist.lastOrigin = hist.lastSelOrigin = change.origin; + + if (!last) signal(doc, "historyAdded"); + } + + function selectionEventCanBeMerged(doc, origin, prev, sel) { + var ch = origin.charAt(0); + return ch == "*" || + ch == "+" && + prev.ranges.length == sel.ranges.length && + prev.somethingSelected() == sel.somethingSelected() && + new Date - doc.history.lastSelTime <= (doc.cm ? doc.cm.options.historyEventDelay : 500); + } + + // Called whenever the selection changes, sets the new selection as + // the pending selection in the history, and pushes the old pending + // selection into the 'done' array when it was significantly + // different (in number of selected ranges, emptiness, or time). + function addSelectionToHistory(doc, sel, opId, options) { + var hist = doc.history, origin = options && options.origin; + + // A new event is started when the previous origin does not match + // the current, or the origins don't allow matching. Origins + // starting with * are always merged, those starting with + are + // merged when similar and close together in time. + if (opId == hist.lastSelOp || + (origin && hist.lastSelOrigin == origin && + (hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin || + selectionEventCanBeMerged(doc, origin, lst(hist.done), sel)))) + hist.done[hist.done.length - 1] = sel; + else + pushSelectionToHistory(sel, hist.done); + + hist.lastSelTime = +new Date; + hist.lastSelOrigin = origin; + hist.lastSelOp = opId; + if (options && options.clearRedo !== false) + clearSelectionEvents(hist.undone); + } + + function pushSelectionToHistory(sel, dest) { + var top = lst(dest); + if (!(top && top.ranges && top.equals(sel))) + dest.push(sel); + } + + // Used to store marked span information in the history. + function attachLocalSpans(doc, change, from, to) { + var existing = change["spans_" + doc.id], n = 0; + doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function(line) { + if (line.markedSpans) + (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans; + ++n; + }); + } + + // When un/re-doing restores text containing marked spans, those + // that have been explicitly cleared should not be restored. + function removeClearedSpans(spans) { + if (!spans) return null; + for (var i = 0, out; i < spans.length; ++i) { + if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i); } + else if (out) out.push(spans[i]); + } + return !out ? spans : out.length ? out : null; + } + + // Retrieve and filter the old marked spans stored in a change event. + function getOldSpans(doc, change) { + var found = change["spans_" + doc.id]; + if (!found) return null; + for (var i = 0, nw = []; i < change.text.length; ++i) + nw.push(removeClearedSpans(found[i])); + return nw; + } + + // Used both to provide a JSON-safe object in .getHistory, and, when + // detaching a document, to split the history in two + function copyHistoryArray(events, newGroup, instantiateSel) { + for (var i = 0, copy = []; i < events.length; ++i) { + var event = events[i]; + if (event.ranges) { + copy.push(instantiateSel ? Selection.prototype.deepCopy.call(event) : event); + continue; + } + var changes = event.changes, newChanges = []; + copy.push({changes: newChanges}); + for (var j = 0; j < changes.length; ++j) { + var change = changes[j], m; + newChanges.push({from: change.from, to: change.to, text: change.text}); + if (newGroup) for (var prop in change) if (m = prop.match(/^spans_(\d+)$/)) { + if (indexOf(newGroup, Number(m[1])) > -1) { + lst(newChanges)[prop] = change[prop]; + delete change[prop]; + } + } + } + } + return copy; + } + + // Rebasing/resetting history to deal with externally-sourced changes + + function rebaseHistSelSingle(pos, from, to, diff) { + if (to < pos.line) { + pos.line += diff; + } else if (from < pos.line) { + pos.line = from; + pos.ch = 0; + } + } + + // Tries to rebase an array of history events given a change in the + // document. If the change touches the same lines as the event, the + // event, and everything 'behind' it, is discarded. If the change is + // before the event, the event's positions are updated. Uses a + // copy-on-write scheme for the positions, to avoid having to + // reallocate them all on every rebase, but also avoid problems with + // shared position objects being unsafely updated. + function rebaseHistArray(array, from, to, diff) { + for (var i = 0; i < array.length; ++i) { + var sub = array[i], ok = true; + if (sub.ranges) { + if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true; } + for (var j = 0; j < sub.ranges.length; j++) { + rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff); + rebaseHistSelSingle(sub.ranges[j].head, from, to, diff); + } + continue; + } + for (var j = 0; j < sub.changes.length; ++j) { + var cur = sub.changes[j]; + if (to < cur.from.line) { + cur.from = Pos(cur.from.line + diff, cur.from.ch); + cur.to = Pos(cur.to.line + diff, cur.to.ch); + } else if (from <= cur.to.line) { + ok = false; + break; + } + } + if (!ok) { + array.splice(0, i + 1); + i = 0; + } + } + } + + function rebaseHist(hist, change) { + var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1; + rebaseHistArray(hist.done, from, to, diff); + rebaseHistArray(hist.undone, from, to, diff); + } + + // EVENT UTILITIES + + // Due to the fact that we still support jurassic IE versions, some + // compatibility wrappers are needed. + + var e_preventDefault = CodeMirror.e_preventDefault = function(e) { + if (e.preventDefault) e.preventDefault(); + else e.returnValue = false; + }; + var e_stopPropagation = CodeMirror.e_stopPropagation = function(e) { + if (e.stopPropagation) e.stopPropagation(); + else e.cancelBubble = true; + }; + function e_defaultPrevented(e) { + return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false; + } + var e_stop = CodeMirror.e_stop = function(e) {e_preventDefault(e); e_stopPropagation(e);}; + + function e_target(e) {return e.target || e.srcElement;} + function e_button(e) { + var b = e.which; + if (b == null) { + if (e.button & 1) b = 1; + else if (e.button & 2) b = 3; + else if (e.button & 4) b = 2; + } + if (mac && e.ctrlKey && b == 1) b = 3; + return b; + } + + // EVENT HANDLING + + // Lightweight event framework. on/off also work on DOM nodes, + // registering native DOM handlers. + + var on = CodeMirror.on = function(emitter, type, f) { + if (emitter.addEventListener) + emitter.addEventListener(type, f, false); + else if (emitter.attachEvent) + emitter.attachEvent("on" + type, f); + else { + var map = emitter._handlers || (emitter._handlers = {}); + var arr = map[type] || (map[type] = []); + arr.push(f); + } + }; + + var noHandlers = [] + function getHandlers(emitter, type, copy) { + var arr = emitter._handlers && emitter._handlers[type] + if (copy) return arr && arr.length > 0 ? arr.slice() : noHandlers + else return arr || noHandlers + } + + var off = CodeMirror.off = function(emitter, type, f) { + if (emitter.removeEventListener) + emitter.removeEventListener(type, f, false); + else if (emitter.detachEvent) + emitter.detachEvent("on" + type, f); + else { + var handlers = getHandlers(emitter, type, false) + for (var i = 0; i < handlers.length; ++i) + if (handlers[i] == f) { handlers.splice(i, 1); break; } + } + }; + + var signal = CodeMirror.signal = function(emitter, type /*, values...*/) { + var handlers = getHandlers(emitter, type, true) + if (!handlers.length) return; + var args = Array.prototype.slice.call(arguments, 2); + for (var i = 0; i < handlers.length; ++i) handlers[i].apply(null, args); + }; + + var orphanDelayedCallbacks = null; + + // Often, we want to signal events at a point where we are in the + // middle of some work, but don't want the handler to start calling + // other methods on the editor, which might be in an inconsistent + // state or simply not expect any other events to happen. + // signalLater looks whether there are any handlers, and schedules + // them to be executed when the last operation ends, or, if no + // operation is active, when a timeout fires. + function signalLater(emitter, type /*, values...*/) { + var arr = getHandlers(emitter, type, false) + if (!arr.length) return; + var args = Array.prototype.slice.call(arguments, 2), list; + if (operationGroup) { + list = operationGroup.delayedCallbacks; + } else if (orphanDelayedCallbacks) { + list = orphanDelayedCallbacks; + } else { + list = orphanDelayedCallbacks = []; + setTimeout(fireOrphanDelayed, 0); + } + function bnd(f) {return function(){f.apply(null, args);};}; + for (var i = 0; i < arr.length; ++i) + list.push(bnd(arr[i])); + } + + function fireOrphanDelayed() { + var delayed = orphanDelayedCallbacks; + orphanDelayedCallbacks = null; + for (var i = 0; i < delayed.length; ++i) delayed[i](); + } + + // The DOM events that CodeMirror handles can be overridden by + // registering a (non-DOM) handler on the editor for the event name, + // and preventDefault-ing the event in that handler. + function signalDOMEvent(cm, e, override) { + if (typeof e == "string") + e = {type: e, preventDefault: function() { this.defaultPrevented = true; }}; + signal(cm, override || e.type, cm, e); + return e_defaultPrevented(e) || e.codemirrorIgnore; + } + + function signalCursorActivity(cm) { + var arr = cm._handlers && cm._handlers.cursorActivity; + if (!arr) return; + var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = []); + for (var i = 0; i < arr.length; ++i) if (indexOf(set, arr[i]) == -1) + set.push(arr[i]); + } + + function hasHandler(emitter, type) { + return getHandlers(emitter, type).length > 0 + } + + // Add on and off methods to a constructor's prototype, to make + // registering events on such objects more convenient. + function eventMixin(ctor) { + ctor.prototype.on = function(type, f) {on(this, type, f);}; + ctor.prototype.off = function(type, f) {off(this, type, f);}; + } + + // MISC UTILITIES + + // Number of pixels added to scroller and sizer to hide scrollbar + var scrollerGap = 30; + + // Returned or thrown by various protocols to signal 'I'm not + // handling this'. + var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}}; + + // Reused option objects for setSelection & friends + var sel_dontScroll = {scroll: false}, sel_mouse = {origin: "*mouse"}, sel_move = {origin: "+move"}; + + function Delayed() {this.id = null;} + Delayed.prototype.set = function(ms, f) { + clearTimeout(this.id); + this.id = setTimeout(f, ms); + }; + + // Counts the column offset in a string, taking tabs into account. + // Used mostly to find indentation. + var countColumn = CodeMirror.countColumn = function(string, end, tabSize, startIndex, startValue) { + if (end == null) { + end = string.search(/[^\s\u00a0]/); + if (end == -1) end = string.length; + } + for (var i = startIndex || 0, n = startValue || 0;;) { + var nextTab = string.indexOf("\t", i); + if (nextTab < 0 || nextTab >= end) + return n + (end - i); + n += nextTab - i; + n += tabSize - (n % tabSize); + i = nextTab + 1; + } + }; + + // The inverse of countColumn -- find the offset that corresponds to + // a particular column. + var findColumn = CodeMirror.findColumn = function(string, goal, tabSize) { + for (var pos = 0, col = 0;;) { + var nextTab = string.indexOf("\t", pos); + if (nextTab == -1) nextTab = string.length; + var skipped = nextTab - pos; + if (nextTab == string.length || col + skipped >= goal) + return pos + Math.min(skipped, goal - col); + col += nextTab - pos; + col += tabSize - (col % tabSize); + pos = nextTab + 1; + if (col >= goal) return pos; + } + } + + var spaceStrs = [""]; + function spaceStr(n) { + while (spaceStrs.length <= n) + spaceStrs.push(lst(spaceStrs) + " "); + return spaceStrs[n]; + } + + function lst(arr) { return arr[arr.length-1]; } + + var selectInput = function(node) { node.select(); }; + if (ios) // Mobile Safari apparently has a bug where select() is broken. + selectInput = function(node) { node.selectionStart = 0; node.selectionEnd = node.value.length; }; + else if (ie) // Suppress mysterious IE10 errors + selectInput = function(node) { try { node.select(); } catch(_e) {} }; + + function indexOf(array, elt) { + for (var i = 0; i < array.length; ++i) + if (array[i] == elt) return i; + return -1; + } + function map(array, f) { + var out = []; + for (var i = 0; i < array.length; i++) out[i] = f(array[i], i); + return out; + } + + function nothing() {} + + function createObj(base, props) { + var inst; + if (Object.create) { + inst = Object.create(base); + } else { + nothing.prototype = base; + inst = new nothing(); + } + if (props) copyObj(props, inst); + return inst; + }; + + function copyObj(obj, target, overwrite) { + if (!target) target = {}; + for (var prop in obj) + if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop))) + target[prop] = obj[prop]; + return target; + } + + function bind(f) { + var args = Array.prototype.slice.call(arguments, 1); + return function(){return f.apply(null, args);}; + } + + var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/; + var isWordCharBasic = CodeMirror.isWordChar = function(ch) { + return /\w/.test(ch) || ch > "\x80" && + (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)); + }; + function isWordChar(ch, helper) { + if (!helper) return isWordCharBasic(ch); + if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) return true; + return helper.test(ch); + } + + function isEmpty(obj) { + for (var n in obj) if (obj.hasOwnProperty(n) && obj[n]) return false; + return true; + } + + // Extending unicode characters. A series of a non-extending char + + // any number of extending chars is treated as a single unit as far + // as editing and measuring is concerned. This is not fully correct, + // since some scripts/fonts/browsers also treat other configurations + // of code points as a group. + var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/; + function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch); } + + // DOM UTILITIES + + function elt(tag, content, className, style) { + var e = document.createElement(tag); + if (className) e.className = className; + if (style) e.style.cssText = style; + if (typeof content == "string") e.appendChild(document.createTextNode(content)); + else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]); + return e; + } + + var range; + if (document.createRange) range = function(node, start, end, endNode) { + var r = document.createRange(); + r.setEnd(endNode || node, end); + r.setStart(node, start); + return r; + }; + else range = function(node, start, end) { + var r = document.body.createTextRange(); + try { r.moveToElementText(node.parentNode); } + catch(e) { return r; } + r.collapse(true); + r.moveEnd("character", end); + r.moveStart("character", start); + return r; + }; + + function removeChildren(e) { + for (var count = e.childNodes.length; count > 0; --count) + e.removeChild(e.firstChild); + return e; + } + + function removeChildrenAndAdd(parent, e) { + return removeChildren(parent).appendChild(e); + } + + var contains = CodeMirror.contains = function(parent, child) { + if (child.nodeType == 3) // Android browser always returns false when child is a textnode + child = child.parentNode; + if (parent.contains) + return parent.contains(child); + do { + if (child.nodeType == 11) child = child.host; + if (child == parent) return true; + } while (child = child.parentNode); + }; + + function activeElt() { + var activeElement = document.activeElement; + while (activeElement && activeElement.root && activeElement.root.activeElement) + activeElement = activeElement.root.activeElement; + return activeElement; + } + // Older versions of IE throws unspecified error when touching + // document.activeElement in some cases (during loading, in iframe) + if (ie && ie_version < 11) activeElt = function() { + try { return document.activeElement; } + catch(e) { return document.body; } + }; + + function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*"); } + var rmClass = CodeMirror.rmClass = function(node, cls) { + var current = node.className; + var match = classTest(cls).exec(current); + if (match) { + var after = current.slice(match.index + match[0].length); + node.className = current.slice(0, match.index) + (after ? match[1] + after : ""); + } + }; + var addClass = CodeMirror.addClass = function(node, cls) { + var current = node.className; + if (!classTest(cls).test(current)) node.className += (current ? " " : "") + cls; + }; + function joinClasses(a, b) { + var as = a.split(" "); + for (var i = 0; i < as.length; i++) + if (as[i] && !classTest(as[i]).test(b)) b += " " + as[i]; + return b; + } + + // WINDOW-WIDE EVENTS + + // These must be handled carefully, because naively registering a + // handler for each editor will cause the editors to never be + // garbage collected. + + function forEachCodeMirror(f) { + if (!document.body.getElementsByClassName) return; + var byClass = document.body.getElementsByClassName("CodeMirror"); + for (var i = 0; i < byClass.length; i++) { + var cm = byClass[i].CodeMirror; + if (cm) f(cm); + } + } + + var globalsRegistered = false; + function ensureGlobalHandlers() { + if (globalsRegistered) return; + registerGlobalHandlers(); + globalsRegistered = true; + } + function registerGlobalHandlers() { + // When the window resizes, we need to refresh active editors. + var resizeTimer; + on(window, "resize", function() { + if (resizeTimer == null) resizeTimer = setTimeout(function() { + resizeTimer = null; + forEachCodeMirror(onResize); + }, 100); + }); + // When the window loses focus, we want to show the editor as blurred + on(window, "blur", function() { + forEachCodeMirror(onBlur); + }); + } + + // FEATURE DETECTION + + // Detect drag-and-drop + var dragAndDrop = function() { + // There is *some* kind of drag-and-drop support in IE6-8, but I + // couldn't get it to work yet. + if (ie && ie_version < 9) return false; + var div = elt('div'); + return "draggable" in div || "dragDrop" in div; + }(); + + var zwspSupported; + function zeroWidthElement(measure) { + if (zwspSupported == null) { + var test = elt("span", "\u200b"); + removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")])); + if (measure.firstChild.offsetHeight != 0) + zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8); + } + var node = zwspSupported ? elt("span", "\u200b") : + elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px"); + node.setAttribute("cm-text", ""); + return node; + } + + // Feature-detect IE's crummy client rect reporting for bidi text + var badBidiRects; + function hasBadBidiRects(measure) { + if (badBidiRects != null) return badBidiRects; + var txt = removeChildrenAndAdd(measure, document.createTextNode("A\u062eA")); + var r0 = range(txt, 0, 1).getBoundingClientRect(); + if (!r0 || r0.left == r0.right) return false; // Safari returns null in some cases (#2780) + var r1 = range(txt, 1, 2).getBoundingClientRect(); + return badBidiRects = (r1.right - r0.right < 3); + } + + // See if "".split is the broken IE version, if so, provide an + // alternative way to split lines. + var splitLinesAuto = CodeMirror.splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) { + var pos = 0, result = [], l = string.length; + while (pos <= l) { + var nl = string.indexOf("\n", pos); + if (nl == -1) nl = string.length; + var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl); + var rt = line.indexOf("\r"); + if (rt != -1) { + result.push(line.slice(0, rt)); + pos += rt + 1; + } else { + result.push(line); + pos = nl + 1; + } + } + return result; + } : function(string){return string.split(/\r\n?|\n/);}; + + var hasSelection = window.getSelection ? function(te) { + try { return te.selectionStart != te.selectionEnd; } + catch(e) { return false; } + } : function(te) { + try {var range = te.ownerDocument.selection.createRange();} + catch(e) {} + if (!range || range.parentElement() != te) return false; + return range.compareEndPoints("StartToEnd", range) != 0; + }; + + var hasCopyEvent = (function() { + var e = elt("div"); + if ("oncopy" in e) return true; + e.setAttribute("oncopy", "return;"); + return typeof e.oncopy == "function"; + })(); + + var badZoomedRects = null; + function hasBadZoomedRects(measure) { + if (badZoomedRects != null) return badZoomedRects; + var node = removeChildrenAndAdd(measure, elt("span", "x")); + var normal = node.getBoundingClientRect(); + var fromRange = range(node, 0, 1).getBoundingClientRect(); + return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1; + } + + // KEY NAMES + + var keyNames = CodeMirror.keyNames = { + 3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", + 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", + 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", + 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", + 106: "*", 107: "=", 109: "-", 110: ".", 111: "/", 127: "Delete", + 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", + 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", + 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert" + }; + (function() { + // Number keys + for (var i = 0; i < 10; i++) keyNames[i + 48] = keyNames[i + 96] = String(i); + // Alphabetic keys + for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i); + // Function keys + for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i; + })(); + + // BIDI HELPERS + + function iterateBidiSections(order, from, to, f) { + if (!order) return f(from, to, "ltr"); + var found = false; + for (var i = 0; i < order.length; ++i) { + var part = order[i]; + if (part.from < to && part.to > from || from == to && part.to == from) { + f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr"); + found = true; + } + } + if (!found) f(from, to, "ltr"); + } + + function bidiLeft(part) { return part.level % 2 ? part.to : part.from; } + function bidiRight(part) { return part.level % 2 ? part.from : part.to; } + + function lineLeft(line) { var order = getOrder(line); return order ? bidiLeft(order[0]) : 0; } + function lineRight(line) { + var order = getOrder(line); + if (!order) return line.text.length; + return bidiRight(lst(order)); + } + + function lineStart(cm, lineN) { + var line = getLine(cm.doc, lineN); + var visual = visualLine(line); + if (visual != line) lineN = lineNo(visual); + var order = getOrder(visual); + var ch = !order ? 0 : order[0].level % 2 ? lineRight(visual) : lineLeft(visual); + return Pos(lineN, ch); + } + function lineEnd(cm, lineN) { + var merged, line = getLine(cm.doc, lineN); + while (merged = collapsedSpanAtEnd(line)) { + line = merged.find(1, true).line; + lineN = null; + } + var order = getOrder(line); + var ch = !order ? line.text.length : order[0].level % 2 ? lineLeft(line) : lineRight(line); + return Pos(lineN == null ? lineNo(line) : lineN, ch); + } + function lineStartSmart(cm, pos) { + var start = lineStart(cm, pos.line); + var line = getLine(cm.doc, start.line); + var order = getOrder(line); + if (!order || order[0].level == 0) { + var firstNonWS = Math.max(0, line.text.search(/\S/)); + var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch; + return Pos(start.line, inWS ? 0 : firstNonWS); + } + return start; + } + + function compareBidiLevel(order, a, b) { + var linedir = order[0].level; + if (a == linedir) return true; + if (b == linedir) return false; + return a < b; + } + var bidiOther; + function getBidiPartAt(order, pos) { + bidiOther = null; + for (var i = 0, found; i < order.length; ++i) { + var cur = order[i]; + if (cur.from < pos && cur.to > pos) return i; + if ((cur.from == pos || cur.to == pos)) { + if (found == null) { + found = i; + } else if (compareBidiLevel(order, cur.level, order[found].level)) { + if (cur.from != cur.to) bidiOther = found; + return i; + } else { + if (cur.from != cur.to) bidiOther = i; + return found; + } + } + } + return found; + } + + function moveInLine(line, pos, dir, byUnit) { + if (!byUnit) return pos + dir; + do pos += dir; + while (pos > 0 && isExtendingChar(line.text.charAt(pos))); + return pos; + } + + // This is needed in order to move 'visually' through bi-directional + // text -- i.e., pressing left should make the cursor go left, even + // when in RTL text. The tricky part is the 'jumps', where RTL and + // LTR text touch each other. This often requires the cursor offset + // to move more than one unit, in order to visually move one unit. + function moveVisually(line, start, dir, byUnit) { + var bidi = getOrder(line); + if (!bidi) return moveLogically(line, start, dir, byUnit); + var pos = getBidiPartAt(bidi, start), part = bidi[pos]; + var target = moveInLine(line, start, part.level % 2 ? -dir : dir, byUnit); + + for (;;) { + if (target > part.from && target < part.to) return target; + if (target == part.from || target == part.to) { + if (getBidiPartAt(bidi, target) == pos) return target; + part = bidi[pos += dir]; + return (dir > 0) == part.level % 2 ? part.to : part.from; + } else { + part = bidi[pos += dir]; + if (!part) return null; + if ((dir > 0) == part.level % 2) + target = moveInLine(line, part.to, -1, byUnit); + else + target = moveInLine(line, part.from, 1, byUnit); + } + } + } + + function moveLogically(line, start, dir, byUnit) { + var target = start + dir; + if (byUnit) while (target > 0 && isExtendingChar(line.text.charAt(target))) target += dir; + return target < 0 || target > line.text.length ? null : target; + } + + // Bidirectional ordering algorithm + // See http://unicode.org/reports/tr9/tr9-13.html for the algorithm + // that this (partially) implements. + + // One-char codes used for character types: + // L (L): Left-to-Right + // R (R): Right-to-Left + // r (AL): Right-to-Left Arabic + // 1 (EN): European Number + // + (ES): European Number Separator + // % (ET): European Number Terminator + // n (AN): Arabic Number + // , (CS): Common Number Separator + // m (NSM): Non-Spacing Mark + // b (BN): Boundary Neutral + // s (B): Paragraph Separator + // t (S): Segment Separator + // w (WS): Whitespace + // N (ON): Other Neutrals + + // Returns null if characters are ordered as they appear + // (left-to-right), or an array of sections ({from, to, level} + // objects) in the order in which they occur visually. + var bidiOrdering = (function() { + // Character types for codepoints 0 to 0xff + var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN"; + // Character types for codepoints 0x600 to 0x6ff + var arabicTypes = "rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmm"; + function charType(code) { + if (code <= 0xf7) return lowTypes.charAt(code); + else if (0x590 <= code && code <= 0x5f4) return "R"; + else if (0x600 <= code && code <= 0x6ed) return arabicTypes.charAt(code - 0x600); + else if (0x6ee <= code && code <= 0x8ac) return "r"; + else if (0x2000 <= code && code <= 0x200b) return "w"; + else if (code == 0x200c) return "b"; + else return "L"; + } + + var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/; + var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/; + // Browsers seem to always treat the boundaries of block elements as being L. + var outerType = "L"; + + function BidiSpan(level, from, to) { + this.level = level; + this.from = from; this.to = to; + } + + return function(str) { + if (!bidiRE.test(str)) return false; + var len = str.length, types = []; + for (var i = 0, type; i < len; ++i) + types.push(type = charType(str.charCodeAt(i))); + + // W1. Examine each non-spacing mark (NSM) in the level run, and + // change the type of the NSM to the type of the previous + // character. If the NSM is at the start of the level run, it will + // get the type of sor. + for (var i = 0, prev = outerType; i < len; ++i) { + var type = types[i]; + if (type == "m") types[i] = prev; + else prev = type; + } + + // W2. Search backwards from each instance of a European number + // until the first strong type (R, L, AL, or sor) is found. If an + // AL is found, change the type of the European number to Arabic + // number. + // W3. Change all ALs to R. + for (var i = 0, cur = outerType; i < len; ++i) { + var type = types[i]; + if (type == "1" && cur == "r") types[i] = "n"; + else if (isStrong.test(type)) { cur = type; if (type == "r") types[i] = "R"; } + } + + // W4. A single European separator between two European numbers + // changes to a European number. A single common separator between + // two numbers of the same type changes to that type. + for (var i = 1, prev = types[0]; i < len - 1; ++i) { + var type = types[i]; + if (type == "+" && prev == "1" && types[i+1] == "1") types[i] = "1"; + else if (type == "," && prev == types[i+1] && + (prev == "1" || prev == "n")) types[i] = prev; + prev = type; + } + + // W5. A sequence of European terminators adjacent to European + // numbers changes to all European numbers. + // W6. Otherwise, separators and terminators change to Other + // Neutral. + for (var i = 0; i < len; ++i) { + var type = types[i]; + if (type == ",") types[i] = "N"; + else if (type == "%") { + for (var end = i + 1; end < len && types[end] == "%"; ++end) {} + var replace = (i && types[i-1] == "!") || (end < len && types[end] == "1") ? "1" : "N"; + for (var j = i; j < end; ++j) types[j] = replace; + i = end - 1; + } + } + + // W7. Search backwards from each instance of a European number + // until the first strong type (R, L, or sor) is found. If an L is + // found, then change the type of the European number to L. + for (var i = 0, cur = outerType; i < len; ++i) { + var type = types[i]; + if (cur == "L" && type == "1") types[i] = "L"; + else if (isStrong.test(type)) cur = type; + } + + // N1. A sequence of neutrals takes the direction of the + // surrounding strong text if the text on both sides has the same + // direction. European and Arabic numbers act as if they were R in + // terms of their influence on neutrals. Start-of-level-run (sor) + // and end-of-level-run (eor) are used at level run boundaries. + // N2. Any remaining neutrals take the embedding direction. + for (var i = 0; i < len; ++i) { + if (isNeutral.test(types[i])) { + for (var end = i + 1; end < len && isNeutral.test(types[end]); ++end) {} + var before = (i ? types[i-1] : outerType) == "L"; + var after = (end < len ? types[end] : outerType) == "L"; + var replace = before || after ? "L" : "R"; + for (var j = i; j < end; ++j) types[j] = replace; + i = end - 1; + } + } + + // Here we depart from the documented algorithm, in order to avoid + // building up an actual levels array. Since there are only three + // levels (0, 1, 2) in an implementation that doesn't take + // explicit embedding into account, we can build up the order on + // the fly, without following the level-based algorithm. + var order = [], m; + for (var i = 0; i < len;) { + if (countsAsLeft.test(types[i])) { + var start = i; + for (++i; i < len && countsAsLeft.test(types[i]); ++i) {} + order.push(new BidiSpan(0, start, i)); + } else { + var pos = i, at = order.length; + for (++i; i < len && types[i] != "L"; ++i) {} + for (var j = pos; j < i;) { + if (countsAsNum.test(types[j])) { + if (pos < j) order.splice(at, 0, new BidiSpan(1, pos, j)); + var nstart = j; + for (++j; j < i && countsAsNum.test(types[j]); ++j) {} + order.splice(at, 0, new BidiSpan(2, nstart, j)); + pos = j; + } else ++j; + } + if (pos < i) order.splice(at, 0, new BidiSpan(1, pos, i)); + } + } + if (order[0].level == 1 && (m = str.match(/^\s+/))) { + order[0].from = m[0].length; + order.unshift(new BidiSpan(0, 0, m[0].length)); + } + if (lst(order).level == 1 && (m = str.match(/\s+$/))) { + lst(order).to -= m[0].length; + order.push(new BidiSpan(0, len - m[0].length, len)); + } + if (order[0].level == 2) + order.unshift(new BidiSpan(1, order[0].to, order[0].to)); + if (order[0].level != lst(order).level) + order.push(new BidiSpan(order[0].level, len, len)); + + return order; + }; + })(); + + // THE END + + CodeMirror.version = "5.16.2"; + + return CodeMirror; +}); + +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + var Pos = CodeMirror.Pos; + function posEq(a, b) { return a.line == b.line && a.ch == b.ch; } + + // Kill 'ring' + + var killRing = []; + function addToRing(str) { + killRing.push(str); + if (killRing.length > 50) killRing.shift(); + } + function growRingTop(str) { + if (!killRing.length) return addToRing(str); + killRing[killRing.length - 1] += str; + } + function getFromRing(n) { return killRing[killRing.length - (n ? Math.min(n, 1) : 1)] || ""; } + function popFromRing() { if (killRing.length > 1) killRing.pop(); return getFromRing(); } + + var lastKill = null; + + function kill(cm, from, to, mayGrow, text) { + if (text == null) text = cm.getRange(from, to); + + if (mayGrow && lastKill && lastKill.cm == cm && posEq(from, lastKill.pos) && cm.isClean(lastKill.gen)) + growRingTop(text); + else + addToRing(text); + cm.replaceRange("", from, to, "+delete"); + + if (mayGrow) lastKill = {cm: cm, pos: from, gen: cm.changeGeneration()}; + else lastKill = null; + } + + // Boundaries of various units + + function byChar(cm, pos, dir) { + return cm.findPosH(pos, dir, "char", true); + } + + function byWord(cm, pos, dir) { + return cm.findPosH(pos, dir, "word", true); + } + + function byLine(cm, pos, dir) { + return cm.findPosV(pos, dir, "line", cm.doc.sel.goalColumn); + } + + function byPage(cm, pos, dir) { + return cm.findPosV(pos, dir, "page", cm.doc.sel.goalColumn); + } + + function byParagraph(cm, pos, dir) { + var no = pos.line, line = cm.getLine(no); + var sawText = /\S/.test(dir < 0 ? line.slice(0, pos.ch) : line.slice(pos.ch)); + var fst = cm.firstLine(), lst = cm.lastLine(); + for (;;) { + no += dir; + if (no < fst || no > lst) + return cm.clipPos(Pos(no - dir, dir < 0 ? 0 : null)); + line = cm.getLine(no); + var hasText = /\S/.test(line); + if (hasText) sawText = true; + else if (sawText) return Pos(no, 0); + } + } + + function bySentence(cm, pos, dir) { + var line = pos.line, ch = pos.ch; + var text = cm.getLine(pos.line), sawWord = false; + for (;;) { + var next = text.charAt(ch + (dir < 0 ? -1 : 0)); + if (!next) { // End/beginning of line reached + if (line == (dir < 0 ? cm.firstLine() : cm.lastLine())) return Pos(line, ch); + text = cm.getLine(line + dir); + if (!/\S/.test(text)) return Pos(line, ch); + line += dir; + ch = dir < 0 ? text.length : 0; + continue; + } + if (sawWord && /[!?.]/.test(next)) return Pos(line, ch + (dir > 0 ? 1 : 0)); + if (!sawWord) sawWord = /\w/.test(next); + ch += dir; + } + } + + function byExpr(cm, pos, dir) { + var wrap; + if (cm.findMatchingBracket && (wrap = cm.findMatchingBracket(pos, true)) + && wrap.match && (wrap.forward ? 1 : -1) == dir) + return dir > 0 ? Pos(wrap.to.line, wrap.to.ch + 1) : wrap.to; + + for (var first = true;; first = false) { + var token = cm.getTokenAt(pos); + var after = Pos(pos.line, dir < 0 ? token.start : token.end); + if (first && dir > 0 && token.end == pos.ch || !/\w/.test(token.string)) { + var newPos = cm.findPosH(after, dir, "char"); + if (posEq(after, newPos)) return pos; + else pos = newPos; + } else { + return after; + } + } + } + + // Prefixes (only crudely supported) + + function getPrefix(cm, precise) { + var digits = cm.state.emacsPrefix; + if (!digits) return precise ? null : 1; + clearPrefix(cm); + return digits == "-" ? -1 : Number(digits); + } + + function repeated(cmd) { + var f = typeof cmd == "string" ? function(cm) { cm.execCommand(cmd); } : cmd; + return function(cm) { + var prefix = getPrefix(cm); + f(cm); + for (var i = 1; i < prefix; ++i) f(cm); + }; + } + + function findEnd(cm, pos, by, dir) { + var prefix = getPrefix(cm); + if (prefix < 0) { dir = -dir; prefix = -prefix; } + for (var i = 0; i < prefix; ++i) { + var newPos = by(cm, pos, dir); + if (posEq(newPos, pos)) break; + pos = newPos; + } + return pos; + } + + function move(by, dir) { + var f = function(cm) { + cm.extendSelection(findEnd(cm, cm.getCursor(), by, dir)); + }; + f.motion = true; + return f; + } + + function killTo(cm, by, dir) { + var selections = cm.listSelections(), cursor; + var i = selections.length; + while (i--) { + cursor = selections[i].head; + kill(cm, cursor, findEnd(cm, cursor, by, dir), true); + } + } + + function killRegion(cm) { + if (cm.somethingSelected()) { + var selections = cm.listSelections(), selection; + var i = selections.length; + while (i--) { + selection = selections[i]; + kill(cm, selection.anchor, selection.head); + } + return true; + } + } + + function addPrefix(cm, digit) { + if (cm.state.emacsPrefix) { + if (digit != "-") cm.state.emacsPrefix += digit; + return; + } + // Not active yet + cm.state.emacsPrefix = digit; + cm.on("keyHandled", maybeClearPrefix); + cm.on("inputRead", maybeDuplicateInput); + } + + var prefixPreservingKeys = {"Alt-G": true, "Ctrl-X": true, "Ctrl-Q": true, "Ctrl-U": true}; + + function maybeClearPrefix(cm, arg) { + if (!cm.state.emacsPrefixMap && !prefixPreservingKeys.hasOwnProperty(arg)) + clearPrefix(cm); + } + + function clearPrefix(cm) { + cm.state.emacsPrefix = null; + cm.off("keyHandled", maybeClearPrefix); + cm.off("inputRead", maybeDuplicateInput); + } + + function maybeDuplicateInput(cm, event) { + var dup = getPrefix(cm); + if (dup > 1 && event.origin == "+input") { + var one = event.text.join("\n"), txt = ""; + for (var i = 1; i < dup; ++i) txt += one; + cm.replaceSelection(txt); + } + } + + function addPrefixMap(cm) { + cm.state.emacsPrefixMap = true; + cm.addKeyMap(prefixMap); + cm.on("keyHandled", maybeRemovePrefixMap); + cm.on("inputRead", maybeRemovePrefixMap); + } + + function maybeRemovePrefixMap(cm, arg) { + if (typeof arg == "string" && (/^\d$/.test(arg) || arg == "Ctrl-U")) return; + cm.removeKeyMap(prefixMap); + cm.state.emacsPrefixMap = false; + cm.off("keyHandled", maybeRemovePrefixMap); + cm.off("inputRead", maybeRemovePrefixMap); + } + + // Utilities + + function setMark(cm) { + cm.setCursor(cm.getCursor()); + cm.setExtending(!cm.getExtending()); + cm.on("change", function() { cm.setExtending(false); }); + } + + function clearMark(cm) { + cm.setExtending(false); + cm.setCursor(cm.getCursor()); + } + + function getInput(cm, msg, f) { + if (cm.openDialog) + cm.openDialog(msg + ": ", f, {bottom: true}); + else + f(prompt(msg, "")); + } + + function operateOnWord(cm, op) { + var start = cm.getCursor(), end = cm.findPosH(start, 1, "word"); + cm.replaceRange(op(cm.getRange(start, end)), start, end); + cm.setCursor(end); + } + + function toEnclosingExpr(cm) { + var pos = cm.getCursor(), line = pos.line, ch = pos.ch; + var stack = []; + while (line >= cm.firstLine()) { + var text = cm.getLine(line); + for (var i = ch == null ? text.length : ch; i > 0;) { + var ch = text.charAt(--i); + if (ch == ")") + stack.push("("); + else if (ch == "]") + stack.push("["); + else if (ch == "}") + stack.push("{"); + else if (/[\(\{\[]/.test(ch) && (!stack.length || stack.pop() != ch)) + return cm.extendSelection(Pos(line, i)); + } + --line; ch = null; + } + } + + function quit(cm) { + cm.execCommand("clearSearch"); + clearMark(cm); + } + + // Actual keymap + + var keyMap = CodeMirror.keyMap.emacs = CodeMirror.normalizeKeyMap({ + "Ctrl-W": function(cm) {kill(cm, cm.getCursor("start"), cm.getCursor("end"));}, + "Ctrl-K": repeated(function(cm) { + var start = cm.getCursor(), end = cm.clipPos(Pos(start.line)); + var text = cm.getRange(start, end); + if (!/\S/.test(text)) { + text += "\n"; + end = Pos(start.line + 1, 0); + } + kill(cm, start, end, true, text); + }), + "Alt-W": function(cm) { + addToRing(cm.getSelection()); + clearMark(cm); + }, + "Ctrl-Y": function(cm) { + var start = cm.getCursor(); + cm.replaceRange(getFromRing(getPrefix(cm)), start, start, "paste"); + cm.setSelection(start, cm.getCursor()); + }, + "Alt-Y": function(cm) {cm.replaceSelection(popFromRing(), "around", "paste");}, + + "Ctrl-Space": setMark, "Ctrl-Shift-2": setMark, + + "Ctrl-F": move(byChar, 1), "Ctrl-B": move(byChar, -1), + "Right": move(byChar, 1), "Left": move(byChar, -1), + "Ctrl-D": function(cm) { killTo(cm, byChar, 1); }, + "Delete": function(cm) { killRegion(cm) || killTo(cm, byChar, 1); }, + "Ctrl-H": function(cm) { killTo(cm, byChar, -1); }, + "Backspace": function(cm) { killRegion(cm) || killTo(cm, byChar, -1); }, + + "Alt-F": move(byWord, 1), "Alt-B": move(byWord, -1), + "Alt-D": function(cm) { killTo(cm, byWord, 1); }, + "Alt-Backspace": function(cm) { killTo(cm, byWord, -1); }, + + "Ctrl-N": move(byLine, 1), "Ctrl-P": move(byLine, -1), + "Down": move(byLine, 1), "Up": move(byLine, -1), + "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", + "End": "goLineEnd", "Home": "goLineStart", + + "Alt-V": move(byPage, -1), "Ctrl-V": move(byPage, 1), + "PageUp": move(byPage, -1), "PageDown": move(byPage, 1), + + "Ctrl-Up": move(byParagraph, -1), "Ctrl-Down": move(byParagraph, 1), + + "Alt-A": move(bySentence, -1), "Alt-E": move(bySentence, 1), + "Alt-K": function(cm) { killTo(cm, bySentence, 1); }, + + "Ctrl-Alt-K": function(cm) { killTo(cm, byExpr, 1); }, + "Ctrl-Alt-Backspace": function(cm) { killTo(cm, byExpr, -1); }, + "Ctrl-Alt-F": move(byExpr, 1), "Ctrl-Alt-B": move(byExpr, -1), + + "Shift-Ctrl-Alt-2": function(cm) { + var cursor = cm.getCursor(); + cm.setSelection(findEnd(cm, cursor, byExpr, 1), cursor); + }, + "Ctrl-Alt-T": function(cm) { + var leftStart = byExpr(cm, cm.getCursor(), -1), leftEnd = byExpr(cm, leftStart, 1); + var rightEnd = byExpr(cm, leftEnd, 1), rightStart = byExpr(cm, rightEnd, -1); + cm.replaceRange(cm.getRange(rightStart, rightEnd) + cm.getRange(leftEnd, rightStart) + + cm.getRange(leftStart, leftEnd), leftStart, rightEnd); + }, + "Ctrl-Alt-U": repeated(toEnclosingExpr), + + "Alt-Space": function(cm) { + var pos = cm.getCursor(), from = pos.ch, to = pos.ch, text = cm.getLine(pos.line); + while (from && /\s/.test(text.charAt(from - 1))) --from; + while (to < text.length && /\s/.test(text.charAt(to))) ++to; + cm.replaceRange(" ", Pos(pos.line, from), Pos(pos.line, to)); + }, + "Ctrl-O": repeated(function(cm) { cm.replaceSelection("\n", "start"); }), + "Ctrl-T": repeated(function(cm) { + cm.execCommand("transposeChars"); + }), + + "Alt-C": repeated(function(cm) { + operateOnWord(cm, function(w) { + var letter = w.search(/\w/); + if (letter == -1) return w; + return w.slice(0, letter) + w.charAt(letter).toUpperCase() + w.slice(letter + 1).toLowerCase(); + }); + }), + "Alt-U": repeated(function(cm) { + operateOnWord(cm, function(w) { return w.toUpperCase(); }); + }), + "Alt-L": repeated(function(cm) { + operateOnWord(cm, function(w) { return w.toLowerCase(); }); + }), + + "Alt-;": "toggleComment", + + "Ctrl-/": repeated("undo"), "Shift-Ctrl--": repeated("undo"), + "Ctrl-Z": repeated("undo"), "Cmd-Z": repeated("undo"), + "Shift-Alt-,": "goDocStart", "Shift-Alt-.": "goDocEnd", + "Ctrl-S": "findNext", "Ctrl-R": "findPrev", "Ctrl-G": quit, "Shift-Alt-5": "replace", + "Alt-/": "autocomplete", + "Ctrl-J": "newlineAndIndent", "Enter": false, "Tab": "indentAuto", + + "Alt-G G": function(cm) { + var prefix = getPrefix(cm, true); + if (prefix != null && prefix > 0) return cm.setCursor(prefix - 1); + + getInput(cm, "Goto line", function(str) { + var num; + if (str && !isNaN(num = Number(str)) && num == (num|0) && num > 0) + cm.setCursor(num - 1); + }); + }, + + "Ctrl-X Tab": function(cm) { + cm.indentSelection(getPrefix(cm, true) || cm.getOption("indentUnit")); + }, + "Ctrl-X Ctrl-X": function(cm) { + cm.setSelection(cm.getCursor("head"), cm.getCursor("anchor")); + }, + "Ctrl-X Ctrl-S": "save", + "Ctrl-X Ctrl-W": "save", + "Ctrl-X S": "saveAll", + "Ctrl-X F": "open", + "Ctrl-X U": repeated("undo"), + "Ctrl-X K": "close", + "Ctrl-X Delete": function(cm) { kill(cm, cm.getCursor(), bySentence(cm, cm.getCursor(), 1), true); }, + "Ctrl-X H": "selectAll", + + "Ctrl-Q Tab": repeated("insertTab"), + "Ctrl-U": addPrefixMap + }); + + var prefixMap = {"Ctrl-G": clearPrefix}; + function regPrefix(d) { + prefixMap[d] = function(cm) { addPrefix(cm, d); }; + keyMap["Ctrl-" + d] = function(cm) { addPrefix(cm, d); }; + prefixPreservingKeys["Ctrl-" + d] = true; + } + for (var i = 0; i < 10; ++i) regPrefix(String(i)); + regPrefix("-"); +}); + +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +/** + * Supported keybindings: + * Too many to list. Refer to defaultKeyMap below. + * + * Supported Ex commands: + * Refer to defaultExCommandMap below. + * + * Registers: unnamed, -, a-z, A-Z, 0-9 + * (Does not respect the special case for number registers when delete + * operator is made with these commands: %, (, ), , /, ?, n, N, {, } ) + * TODO: Implement the remaining registers. + * + * Marks: a-z, A-Z, and 0-9 + * TODO: Implement the remaining special marks. They have more complex + * behavior. + * + * Events: + * 'vim-mode-change' - raised on the editor anytime the current mode changes, + * Event object: {mode: "visual", subMode: "linewise"} + * + * Code structure: + * 1. Default keymap + * 2. Variable declarations and short basic helpers + * 3. Instance (External API) implementation + * 4. Internal state tracking objects (input state, counter) implementation + * and instantiation + * 5. Key handler (the main command dispatcher) implementation + * 6. Motion, operator, and action implementations + * 7. Helper functions for the key handler, motions, operators, and actions + * 8. Set up Vim to work as a keymap for CodeMirror. + * 9. Ex command implementations. + */ + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../lib/codemirror"), require("../addon/search/searchcursor"), require("../addon/dialog/dialog"), require("../addon/edit/matchbrackets.js")); + else if (typeof define == "function" && define.amd) // AMD + define(["../lib/codemirror", "../addon/search/searchcursor", "../addon/dialog/dialog", "../addon/edit/matchbrackets"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + 'use strict'; + + var defaultKeymap = [ + // Key to key mapping. This goes first to make it possible to override + // existing mappings. + { keys: '', type: 'keyToKey', toKeys: 'h' }, + { keys: '', type: 'keyToKey', toKeys: 'l' }, + { keys: '', type: 'keyToKey', toKeys: 'k' }, + { keys: '', type: 'keyToKey', toKeys: 'j' }, + { keys: '', type: 'keyToKey', toKeys: 'l' }, + { keys: '', type: 'keyToKey', toKeys: 'h', context: 'normal'}, + { keys: '', type: 'keyToKey', toKeys: 'W' }, + { keys: '', type: 'keyToKey', toKeys: 'B', context: 'normal' }, + { keys: '', type: 'keyToKey', toKeys: 'w' }, + { keys: '', type: 'keyToKey', toKeys: 'b', context: 'normal' }, + { keys: '', type: 'keyToKey', toKeys: 'j' }, + { keys: '', type: 'keyToKey', toKeys: 'k' }, + { keys: '', type: 'keyToKey', toKeys: '' }, + { keys: '', type: 'keyToKey', toKeys: '' }, + { keys: '', type: 'keyToKey', toKeys: '', context: 'insert' }, + { keys: '', type: 'keyToKey', toKeys: '', context: 'insert' }, + { keys: 's', type: 'keyToKey', toKeys: 'cl', context: 'normal' }, + { keys: 's', type: 'keyToKey', toKeys: 'c', context: 'visual'}, + { keys: 'S', type: 'keyToKey', toKeys: 'cc', context: 'normal' }, + { keys: 'S', type: 'keyToKey', toKeys: 'VdO', context: 'visual' }, + { keys: '', type: 'keyToKey', toKeys: '0' }, + { keys: '', type: 'keyToKey', toKeys: '$' }, + { keys: '', type: 'keyToKey', toKeys: '' }, + { keys: '', type: 'keyToKey', toKeys: '' }, + { keys: '', type: 'keyToKey', toKeys: 'j^', context: 'normal' }, + { keys: '', type: 'action', action: 'toggleOverwrite', context: 'insert' }, + // Motions + { keys: 'H', type: 'motion', motion: 'moveToTopLine', motionArgs: { linewise: true, toJumplist: true }}, + { keys: 'M', type: 'motion', motion: 'moveToMiddleLine', motionArgs: { linewise: true, toJumplist: true }}, + { keys: 'L', type: 'motion', motion: 'moveToBottomLine', motionArgs: { linewise: true, toJumplist: true }}, + { keys: 'h', type: 'motion', motion: 'moveByCharacters', motionArgs: { forward: false }}, + { keys: 'l', type: 'motion', motion: 'moveByCharacters', motionArgs: { forward: true }}, + { keys: 'j', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, linewise: true }}, + { keys: 'k', type: 'motion', motion: 'moveByLines', motionArgs: { forward: false, linewise: true }}, + { keys: 'gj', type: 'motion', motion: 'moveByDisplayLines', motionArgs: { forward: true }}, + { keys: 'gk', type: 'motion', motion: 'moveByDisplayLines', motionArgs: { forward: false }}, + { keys: 'w', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: false }}, + { keys: 'W', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: false, bigWord: true }}, + { keys: 'e', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: true, inclusive: true }}, + { keys: 'E', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: true, bigWord: true, inclusive: true }}, + { keys: 'b', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false }}, + { keys: 'B', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false, bigWord: true }}, + { keys: 'ge', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: true, inclusive: true }}, + { keys: 'gE', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: true, bigWord: true, inclusive: true }}, + { keys: '{', type: 'motion', motion: 'moveByParagraph', motionArgs: { forward: false, toJumplist: true }}, + { keys: '}', type: 'motion', motion: 'moveByParagraph', motionArgs: { forward: true, toJumplist: true }}, + { keys: '', type: 'motion', motion: 'moveByPage', motionArgs: { forward: true }}, + { keys: '', type: 'motion', motion: 'moveByPage', motionArgs: { forward: false }}, + { keys: '', type: 'motion', motion: 'moveByScroll', motionArgs: { forward: true, explicitRepeat: true }}, + { keys: '', type: 'motion', motion: 'moveByScroll', motionArgs: { forward: false, explicitRepeat: true }}, + { keys: 'gg', type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: false, explicitRepeat: true, linewise: true, toJumplist: true }}, + { keys: 'G', type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: true, explicitRepeat: true, linewise: true, toJumplist: true }}, + { keys: '0', type: 'motion', motion: 'moveToStartOfLine' }, + { keys: '^', type: 'motion', motion: 'moveToFirstNonWhiteSpaceCharacter' }, + { keys: '+', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, toFirstChar:true }}, + { keys: '-', type: 'motion', motion: 'moveByLines', motionArgs: { forward: false, toFirstChar:true }}, + { keys: '_', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, toFirstChar:true, repeatOffset:-1 }}, + { keys: '$', type: 'motion', motion: 'moveToEol', motionArgs: { inclusive: true }}, + { keys: '%', type: 'motion', motion: 'moveToMatchedSymbol', motionArgs: { inclusive: true, toJumplist: true }}, + { keys: 'f', type: 'motion', motion: 'moveToCharacter', motionArgs: { forward: true , inclusive: true }}, + { keys: 'F', type: 'motion', motion: 'moveToCharacter', motionArgs: { forward: false }}, + { keys: 't', type: 'motion', motion: 'moveTillCharacter', motionArgs: { forward: true, inclusive: true }}, + { keys: 'T', type: 'motion', motion: 'moveTillCharacter', motionArgs: { forward: false }}, + { keys: ';', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: true }}, + { keys: ',', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: false }}, + { keys: '\'', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true, linewise: true}}, + { keys: '`', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true}}, + { keys: ']`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true } }, + { keys: '[`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false } }, + { keys: ']\'', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true, linewise: true } }, + { keys: '[\'', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false, linewise: true } }, + // the next two aren't motions but must come before more general motion declarations + { keys: ']p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: true, isEdit: true, matchIndent: true}}, + { keys: '[p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: false, isEdit: true, matchIndent: true}}, + { keys: ']', type: 'motion', motion: 'moveToSymbol', motionArgs: { forward: true, toJumplist: true}}, + { keys: '[', type: 'motion', motion: 'moveToSymbol', motionArgs: { forward: false, toJumplist: true}}, + { keys: '|', type: 'motion', motion: 'moveToColumn'}, + { keys: 'o', type: 'motion', motion: 'moveToOtherHighlightedEnd', context:'visual'}, + { keys: 'O', type: 'motion', motion: 'moveToOtherHighlightedEnd', motionArgs: {sameLine: true}, context:'visual'}, + // Operators + { keys: 'd', type: 'operator', operator: 'delete' }, + { keys: 'y', type: 'operator', operator: 'yank' }, + { keys: 'c', type: 'operator', operator: 'change' }, + { keys: '>', type: 'operator', operator: 'indent', operatorArgs: { indentRight: true }}, + { keys: '<', type: 'operator', operator: 'indent', operatorArgs: { indentRight: false }}, + { keys: 'g~', type: 'operator', operator: 'changeCase' }, + { keys: 'gu', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: true}, isEdit: true }, + { keys: 'gU', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, isEdit: true }, + { keys: 'n', type: 'motion', motion: 'findNext', motionArgs: { forward: true, toJumplist: true }}, + { keys: 'N', type: 'motion', motion: 'findNext', motionArgs: { forward: false, toJumplist: true }}, + // Operator-Motion dual commands + { keys: 'x', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorMotionArgs: { visualLine: false }}, + { keys: 'X', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: false }, operatorMotionArgs: { visualLine: true }}, + { keys: 'D', type: 'operatorMotion', operator: 'delete', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'}, + { keys: 'D', type: 'operator', operator: 'delete', operatorArgs: { linewise: true }, context: 'visual'}, + { keys: 'Y', type: 'operatorMotion', operator: 'yank', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'}, + { keys: 'Y', type: 'operator', operator: 'yank', operatorArgs: { linewise: true }, context: 'visual'}, + { keys: 'C', type: 'operatorMotion', operator: 'change', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'}, + { keys: 'C', type: 'operator', operator: 'change', operatorArgs: { linewise: true }, context: 'visual'}, + { keys: '~', type: 'operatorMotion', operator: 'changeCase', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorArgs: { shouldMoveCursor: true }, context: 'normal'}, + { keys: '~', type: 'operator', operator: 'changeCase', context: 'visual'}, + { keys: '', type: 'operatorMotion', operator: 'delete', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false }, context: 'insert' }, + // Actions + { keys: '', type: 'action', action: 'jumpListWalk', actionArgs: { forward: true }}, + { keys: '', type: 'action', action: 'jumpListWalk', actionArgs: { forward: false }}, + { keys: '', type: 'action', action: 'scroll', actionArgs: { forward: true, linewise: true }}, + { keys: '', type: 'action', action: 'scroll', actionArgs: { forward: false, linewise: true }}, + { keys: 'a', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'charAfter' }, context: 'normal' }, + { keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'eol' }, context: 'normal' }, + { keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'endOfSelectedArea' }, context: 'visual' }, + { keys: 'i', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'inplace' }, context: 'normal' }, + { keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'firstNonBlank'}, context: 'normal' }, + { keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'startOfSelectedArea' }, context: 'visual' }, + { keys: 'o', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: true }, context: 'normal' }, + { keys: 'O', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: false }, context: 'normal' }, + { keys: 'v', type: 'action', action: 'toggleVisualMode' }, + { keys: 'V', type: 'action', action: 'toggleVisualMode', actionArgs: { linewise: true }}, + { keys: '', type: 'action', action: 'toggleVisualMode', actionArgs: { blockwise: true }}, + { keys: '', type: 'action', action: 'toggleVisualMode', actionArgs: { blockwise: true }}, + { keys: 'gv', type: 'action', action: 'reselectLastSelection' }, + { keys: 'J', type: 'action', action: 'joinLines', isEdit: true }, + { keys: 'p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: true, isEdit: true }}, + { keys: 'P', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: false, isEdit: true }}, + { keys: 'r', type: 'action', action: 'replace', isEdit: true }, + { keys: '@', type: 'action', action: 'replayMacro' }, + { keys: 'q', type: 'action', action: 'enterMacroRecordMode' }, + // Handle Replace-mode as a special case of insert mode. + { keys: 'R', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { replace: true }}, + { keys: 'u', type: 'action', action: 'undo', context: 'normal' }, + { keys: 'u', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: true}, context: 'visual', isEdit: true }, + { keys: 'U', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, context: 'visual', isEdit: true }, + { keys: '', type: 'action', action: 'redo' }, + { keys: 'm', type: 'action', action: 'setMark' }, + { keys: '"', type: 'action', action: 'setRegister' }, + { keys: 'zz', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }}, + { keys: 'z.', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }, motion: 'moveToFirstNonWhiteSpaceCharacter' }, + { keys: 'zt', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }}, + { keys: 'z', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }, motion: 'moveToFirstNonWhiteSpaceCharacter' }, + { keys: 'z-', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }}, + { keys: 'zb', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }, motion: 'moveToFirstNonWhiteSpaceCharacter' }, + { keys: '.', type: 'action', action: 'repeatLastEdit' }, + { keys: '', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: true, backtrack: false}}, + { keys: '', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: false, backtrack: false}}, + // Text object motions + { keys: 'a', type: 'motion', motion: 'textObjectManipulation' }, + { keys: 'i', type: 'motion', motion: 'textObjectManipulation', motionArgs: { textObjectInner: true }}, + // Search + { keys: '/', type: 'search', searchArgs: { forward: true, querySrc: 'prompt', toJumplist: true }}, + { keys: '?', type: 'search', searchArgs: { forward: false, querySrc: 'prompt', toJumplist: true }}, + { keys: '*', type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }}, + { keys: '#', type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }}, + { keys: 'g*', type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor', toJumplist: true }}, + { keys: 'g#', type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', toJumplist: true }}, + // Ex command + { keys: ':', type: 'ex' } + ]; + + /** + * Ex commands + * Care must be taken when adding to the default Ex command map. For any + * pair of commands that have a shared prefix, at least one of their + * shortNames must not match the prefix of the other command. + */ + var defaultExCommandMap = [ + { name: 'colorscheme', shortName: 'colo' }, + { name: 'map' }, + { name: 'imap', shortName: 'im' }, + { name: 'nmap', shortName: 'nm' }, + { name: 'vmap', shortName: 'vm' }, + { name: 'unmap' }, + { name: 'write', shortName: 'w' }, + { name: 'undo', shortName: 'u' }, + { name: 'redo', shortName: 'red' }, + { name: 'set', shortName: 'se' }, + { name: 'set', shortName: 'se' }, + { name: 'setlocal', shortName: 'setl' }, + { name: 'setglobal', shortName: 'setg' }, + { name: 'sort', shortName: 'sor' }, + { name: 'substitute', shortName: 's', possiblyAsync: true }, + { name: 'nohlsearch', shortName: 'noh' }, + { name: 'yank', shortName: 'y' }, + { name: 'delmarks', shortName: 'delm' }, + { name: 'registers', shortName: 'reg', excludeFromCommandHistory: true }, + { name: 'global', shortName: 'g' } + ]; + + var Pos = CodeMirror.Pos; + + var Vim = function() { + function enterVimMode(cm) { + cm.setOption('disableInput', true); + cm.setOption('showCursorWhenSelecting', false); + CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); + cm.on('cursorActivity', onCursorActivity); + maybeInitVimState(cm); + CodeMirror.on(cm.getInputField(), 'paste', getOnPasteFn(cm)); + } + + function leaveVimMode(cm) { + cm.setOption('disableInput', false); + cm.off('cursorActivity', onCursorActivity); + CodeMirror.off(cm.getInputField(), 'paste', getOnPasteFn(cm)); + cm.state.vim = null; + } + + function detachVimMap(cm, next) { + if (this == CodeMirror.keyMap.vim) + CodeMirror.rmClass(cm.getWrapperElement(), "cm-fat-cursor"); + + if (!next || next.attach != attachVimMap) + leaveVimMode(cm, false); + } + function attachVimMap(cm, prev) { + if (this == CodeMirror.keyMap.vim) + CodeMirror.addClass(cm.getWrapperElement(), "cm-fat-cursor"); + + if (!prev || prev.attach != attachVimMap) + enterVimMode(cm); + } + + // Deprecated, simply setting the keymap works again. + CodeMirror.defineOption('vimMode', false, function(cm, val, prev) { + if (val && cm.getOption("keyMap") != "vim") + cm.setOption("keyMap", "vim"); + else if (!val && prev != CodeMirror.Init && /^vim/.test(cm.getOption("keyMap"))) + cm.setOption("keyMap", "default"); + }); + + function cmKey(key, cm) { + if (!cm) { return undefined; } + if (this[key]) { return this[key]; } + var vimKey = cmKeyToVimKey(key); + if (!vimKey) { + return false; + } + var cmd = CodeMirror.Vim.findKey(cm, vimKey); + if (typeof cmd == 'function') { + CodeMirror.signal(cm, 'vim-keypress', vimKey); + } + return cmd; + } + + var modifiers = {'Shift': 'S', 'Ctrl': 'C', 'Alt': 'A', 'Cmd': 'D', 'Mod': 'A'}; + var specialKeys = {Enter:'CR',Backspace:'BS',Delete:'Del',Insert:'Ins'}; + function cmKeyToVimKey(key) { + if (key.charAt(0) == '\'') { + // Keypress character binding of format "'a'" + return key.charAt(1); + } + var pieces = key.split(/-(?!$)/); + var lastPiece = pieces[pieces.length - 1]; + if (pieces.length == 1 && pieces[0].length == 1) { + // No-modifier bindings use literal character bindings above. Skip. + return false; + } else if (pieces.length == 2 && pieces[0] == 'Shift' && lastPiece.length == 1) { + // Ignore Shift+char bindings as they should be handled by literal character. + return false; + } + var hasCharacter = false; + for (var i = 0; i < pieces.length; i++) { + var piece = pieces[i]; + if (piece in modifiers) { pieces[i] = modifiers[piece]; } + else { hasCharacter = true; } + if (piece in specialKeys) { pieces[i] = specialKeys[piece]; } + } + if (!hasCharacter) { + // Vim does not support modifier only keys. + return false; + } + // TODO: Current bindings expect the character to be lower case, but + // it looks like vim key notation uses upper case. + if (isUpperCase(lastPiece)) { + pieces[pieces.length - 1] = lastPiece.toLowerCase(); + } + return '<' + pieces.join('-') + '>'; + } + + function getOnPasteFn(cm) { + var vim = cm.state.vim; + if (!vim.onPasteFn) { + vim.onPasteFn = function() { + if (!vim.insertMode) { + cm.setCursor(offsetCursor(cm.getCursor(), 0, 1)); + actions.enterInsertMode(cm, {}, vim); + } + }; + } + return vim.onPasteFn; + } + + var numberRegex = /[\d]/; + var wordCharTest = [CodeMirror.isWordChar, function(ch) { + return ch && !CodeMirror.isWordChar(ch) && !/\s/.test(ch); + }], bigWordCharTest = [function(ch) { + return /\S/.test(ch); + }]; + function makeKeyRange(start, size) { + var keys = []; + for (var i = start; i < start + size; i++) { + keys.push(String.fromCharCode(i)); + } + return keys; + } + var upperCaseAlphabet = makeKeyRange(65, 26); + var lowerCaseAlphabet = makeKeyRange(97, 26); + var numbers = makeKeyRange(48, 10); + var validMarks = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['<', '>']); + var validRegisters = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['-', '"', '.', ':', '/']); + + function isLine(cm, line) { + return line >= cm.firstLine() && line <= cm.lastLine(); + } + function isLowerCase(k) { + return (/^[a-z]$/).test(k); + } + function isMatchableSymbol(k) { + return '()[]{}'.indexOf(k) != -1; + } + function isNumber(k) { + return numberRegex.test(k); + } + function isUpperCase(k) { + return (/^[A-Z]$/).test(k); + } + function isWhiteSpaceString(k) { + return (/^\s*$/).test(k); + } + function inArray(val, arr) { + for (var i = 0; i < arr.length; i++) { + if (arr[i] == val) { + return true; + } + } + return false; + } + + var options = {}; + function defineOption(name, defaultValue, type, aliases, callback) { + if (defaultValue === undefined && !callback) { + throw Error('defaultValue is required unless callback is provided'); + } + if (!type) { type = 'string'; } + options[name] = { + type: type, + defaultValue: defaultValue, + callback: callback + }; + if (aliases) { + for (var i = 0; i < aliases.length; i++) { + options[aliases[i]] = options[name]; + } + } + if (defaultValue) { + setOption(name, defaultValue); + } + } + + function setOption(name, value, cm, cfg) { + var option = options[name]; + cfg = cfg || {}; + var scope = cfg.scope; + if (!option) { + throw Error('Unknown option: ' + name); + } + if (option.type == 'boolean') { + if (value && value !== true) { + throw Error('Invalid argument: ' + name + '=' + value); + } else if (value !== false) { + // Boolean options are set to true if value is not defined. + value = true; + } + } + if (option.callback) { + if (scope !== 'local') { + option.callback(value, undefined); + } + if (scope !== 'global' && cm) { + option.callback(value, cm); + } + } else { + if (scope !== 'local') { + option.value = option.type == 'boolean' ? !!value : value; + } + if (scope !== 'global' && cm) { + cm.state.vim.options[name] = {value: value}; + } + } + } + + function getOption(name, cm, cfg) { + var option = options[name]; + cfg = cfg || {}; + var scope = cfg.scope; + if (!option) { + throw Error('Unknown option: ' + name); + } + if (option.callback) { + var local = cm && option.callback(undefined, cm); + if (scope !== 'global' && local !== undefined) { + return local; + } + if (scope !== 'local') { + return option.callback(); + } + return; + } else { + var local = (scope !== 'global') && (cm && cm.state.vim.options[name]); + return (local || (scope !== 'local') && option || {}).value; + } + } + + defineOption('filetype', undefined, 'string', ['ft'], function(name, cm) { + // Option is local. Do nothing for global. + if (cm === undefined) { + return; + } + // The 'filetype' option proxies to the CodeMirror 'mode' option. + if (name === undefined) { + var mode = cm.getOption('mode'); + return mode == 'null' ? '' : mode; + } else { + var mode = name == '' ? 'null' : name; + cm.setOption('mode', mode); + } + }); + + var createCircularJumpList = function() { + var size = 100; + var pointer = -1; + var head = 0; + var tail = 0; + var buffer = new Array(size); + function add(cm, oldCur, newCur) { + var current = pointer % size; + var curMark = buffer[current]; + function useNextSlot(cursor) { + var next = ++pointer % size; + var trashMark = buffer[next]; + if (trashMark) { + trashMark.clear(); + } + buffer[next] = cm.setBookmark(cursor); + } + if (curMark) { + var markPos = curMark.find(); + // avoid recording redundant cursor position + if (markPos && !cursorEqual(markPos, oldCur)) { + useNextSlot(oldCur); + } + } else { + useNextSlot(oldCur); + } + useNextSlot(newCur); + head = pointer; + tail = pointer - size + 1; + if (tail < 0) { + tail = 0; + } + } + function move(cm, offset) { + pointer += offset; + if (pointer > head) { + pointer = head; + } else if (pointer < tail) { + pointer = tail; + } + var mark = buffer[(size + pointer) % size]; + // skip marks that are temporarily removed from text buffer + if (mark && !mark.find()) { + var inc = offset > 0 ? 1 : -1; + var newCur; + var oldCur = cm.getCursor(); + do { + pointer += inc; + mark = buffer[(size + pointer) % size]; + // skip marks that are the same as current position + if (mark && + (newCur = mark.find()) && + !cursorEqual(oldCur, newCur)) { + break; + } + } while (pointer < head && pointer > tail); + } + return mark; + } + return { + cachedCursor: undefined, //used for # and * jumps + add: add, + move: move + }; + }; + + // Returns an object to track the changes associated insert mode. It + // clones the object that is passed in, or creates an empty object one if + // none is provided. + var createInsertModeChanges = function(c) { + if (c) { + // Copy construction + return { + changes: c.changes, + expectCursorActivityForChange: c.expectCursorActivityForChange + }; + } + return { + // Change list + changes: [], + // Set to true on change, false on cursorActivity. + expectCursorActivityForChange: false + }; + }; + + function MacroModeState() { + this.latestRegister = undefined; + this.isPlaying = false; + this.isRecording = false; + this.replaySearchQueries = []; + this.onRecordingDone = undefined; + this.lastInsertModeChanges = createInsertModeChanges(); + } + MacroModeState.prototype = { + exitMacroRecordMode: function() { + var macroModeState = vimGlobalState.macroModeState; + if (macroModeState.onRecordingDone) { + macroModeState.onRecordingDone(); // close dialog + } + macroModeState.onRecordingDone = undefined; + macroModeState.isRecording = false; + }, + enterMacroRecordMode: function(cm, registerName) { + var register = + vimGlobalState.registerController.getRegister(registerName); + if (register) { + register.clear(); + this.latestRegister = registerName; + if (cm.openDialog) { + this.onRecordingDone = cm.openDialog( + '(recording)['+registerName+']', null, {bottom:true}); + } + this.isRecording = true; + } + } + }; + + function maybeInitVimState(cm) { + if (!cm.state.vim) { + // Store instance state in the CodeMirror object. + cm.state.vim = { + inputState: new InputState(), + // Vim's input state that triggered the last edit, used to repeat + // motions and operators with '.'. + lastEditInputState: undefined, + // Vim's action command before the last edit, used to repeat actions + // with '.' and insert mode repeat. + lastEditActionCommand: undefined, + // When using jk for navigation, if you move from a longer line to a + // shorter line, the cursor may clip to the end of the shorter line. + // If j is pressed again and cursor goes to the next line, the + // cursor should go back to its horizontal position on the longer + // line if it can. This is to keep track of the horizontal position. + lastHPos: -1, + // Doing the same with screen-position for gj/gk + lastHSPos: -1, + // The last motion command run. Cleared if a non-motion command gets + // executed in between. + lastMotion: null, + marks: {}, + // Mark for rendering fake cursor for visual mode. + fakeCursor: null, + insertMode: false, + // Repeat count for changes made in insert mode, triggered by key + // sequences like 3,i. Only exists when insertMode is true. + insertModeRepeat: undefined, + visualMode: false, + // If we are in visual line mode. No effect if visualMode is false. + visualLine: false, + visualBlock: false, + lastSelection: null, + lastPastedText: null, + sel: {}, + // Buffer-local/window-local values of vim options. + options: {} + }; + } + return cm.state.vim; + } + var vimGlobalState; + function resetVimGlobalState() { + vimGlobalState = { + // The current search query. + searchQuery: null, + // Whether we are searching backwards. + searchIsReversed: false, + // Replace part of the last substituted pattern + lastSubstituteReplacePart: undefined, + jumpList: createCircularJumpList(), + macroModeState: new MacroModeState, + // Recording latest f, t, F or T motion command. + lastCharacterSearch: {increment:0, forward:true, selectedCharacter:''}, + registerController: new RegisterController({}), + // search history buffer + searchHistoryController: new HistoryController({}), + // ex Command history buffer + exCommandHistoryController : new HistoryController({}) + }; + for (var optionName in options) { + var option = options[optionName]; + option.value = option.defaultValue; + } + } + + var lastInsertModeKeyTimer; + var vimApi= { + buildKeyMap: function() { + // TODO: Convert keymap into dictionary format for fast lookup. + }, + // Testing hook, though it might be useful to expose the register + // controller anyways. + getRegisterController: function() { + return vimGlobalState.registerController; + }, + // Testing hook. + resetVimGlobalState_: resetVimGlobalState, + + // Testing hook. + getVimGlobalState_: function() { + return vimGlobalState; + }, + + // Testing hook. + maybeInitVimState_: maybeInitVimState, + + suppressErrorLogging: false, + + InsertModeKey: InsertModeKey, + map: function(lhs, rhs, ctx) { + // Add user defined key bindings. + exCommandDispatcher.map(lhs, rhs, ctx); + }, + unmap: function(lhs, ctx) { + exCommandDispatcher.unmap(lhs, ctx); + }, + // TODO: Expose setOption and getOption as instance methods. Need to decide how to namespace + // them, or somehow make them work with the existing CodeMirror setOption/getOption API. + setOption: setOption, + getOption: getOption, + defineOption: defineOption, + defineEx: function(name, prefix, func){ + if (!prefix) { + prefix = name; + } else if (name.indexOf(prefix) !== 0) { + throw new Error('(Vim.defineEx) "'+prefix+'" is not a prefix of "'+name+'", command not registered'); + } + exCommands[name]=func; + exCommandDispatcher.commandMap_[prefix]={name:name, shortName:prefix, type:'api'}; + }, + handleKey: function (cm, key, origin) { + var command = this.findKey(cm, key, origin); + if (typeof command === 'function') { + return command(); + } + }, + /** + * This is the outermost function called by CodeMirror, after keys have + * been mapped to their Vim equivalents. + * + * Finds a command based on the key (and cached keys if there is a + * multi-key sequence). Returns `undefined` if no key is matched, a noop + * function if a partial match is found (multi-key), and a function to + * execute the bound command if a a key is matched. The function always + * returns true. + */ + findKey: function(cm, key, origin) { + var vim = maybeInitVimState(cm); + function handleMacroRecording() { + var macroModeState = vimGlobalState.macroModeState; + if (macroModeState.isRecording) { + if (key == 'q') { + macroModeState.exitMacroRecordMode(); + clearInputState(cm); + return true; + } + if (origin != 'mapping') { + logKey(macroModeState, key); + } + } + } + function handleEsc() { + if (key == '') { + // Clear input state and get back to normal mode. + clearInputState(cm); + if (vim.visualMode) { + exitVisualMode(cm); + } else if (vim.insertMode) { + exitInsertMode(cm); + } + return true; + } + } + function doKeyToKey(keys) { + // TODO: prevent infinite recursion. + var match; + while (keys) { + // Pull off one command key, which is either a single character + // or a special sequence wrapped in '<' and '>', e.g. ''. + match = (/<\w+-.+?>|<\w+>|./).exec(keys); + key = match[0]; + keys = keys.substring(match.index + key.length); + CodeMirror.Vim.handleKey(cm, key, 'mapping'); + } + } + + function handleKeyInsertMode() { + if (handleEsc()) { return true; } + var keys = vim.inputState.keyBuffer = vim.inputState.keyBuffer + key; + var keysAreChars = key.length == 1; + var match = commandDispatcher.matchCommand(keys, defaultKeymap, vim.inputState, 'insert'); + // Need to check all key substrings in insert mode. + while (keys.length > 1 && match.type != 'full') { + var keys = vim.inputState.keyBuffer = keys.slice(1); + var thisMatch = commandDispatcher.matchCommand(keys, defaultKeymap, vim.inputState, 'insert'); + if (thisMatch.type != 'none') { match = thisMatch; } + } + if (match.type == 'none') { clearInputState(cm); return false; } + else if (match.type == 'partial') { + if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); } + lastInsertModeKeyTimer = window.setTimeout( + function() { if (vim.insertMode && vim.inputState.keyBuffer) { clearInputState(cm); } }, + getOption('insertModeEscKeysTimeout')); + return !keysAreChars; + } + + if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); } + if (keysAreChars) { + var here = cm.getCursor(); + cm.replaceRange('', offsetCursor(here, 0, -(keys.length - 1)), here, '+input'); + } + clearInputState(cm); + return match.command; + } + + function handleKeyNonInsertMode() { + if (handleMacroRecording() || handleEsc()) { return true; }; + + var keys = vim.inputState.keyBuffer = vim.inputState.keyBuffer + key; + if (/^[1-9]\d*$/.test(keys)) { return true; } + + var keysMatcher = /^(\d*)(.*)$/.exec(keys); + if (!keysMatcher) { clearInputState(cm); return false; } + var context = vim.visualMode ? 'visual' : + 'normal'; + var match = commandDispatcher.matchCommand(keysMatcher[2] || keysMatcher[1], defaultKeymap, vim.inputState, context); + if (match.type == 'none') { clearInputState(cm); return false; } + else if (match.type == 'partial') { return true; } + + vim.inputState.keyBuffer = ''; + var keysMatcher = /^(\d*)(.*)$/.exec(keys); + if (keysMatcher[1] && keysMatcher[1] != '0') { + vim.inputState.pushRepeatDigit(keysMatcher[1]); + } + return match.command; + } + + var command; + if (vim.insertMode) { command = handleKeyInsertMode(); } + else { command = handleKeyNonInsertMode(); } + if (command === false) { + return undefined; + } else if (command === true) { + // TODO: Look into using CodeMirror's multi-key handling. + // Return no-op since we are caching the key. Counts as handled, but + // don't want act on it just yet. + return function() {}; + } else { + return function() { + return cm.operation(function() { + cm.curOp.isVimOp = true; + try { + if (command.type == 'keyToKey') { + doKeyToKey(command.toKeys); + } else { + commandDispatcher.processCommand(cm, vim, command); + } + } catch (e) { + // clear VIM state in case it's in a bad state. + cm.state.vim = undefined; + maybeInitVimState(cm); + if (!CodeMirror.Vim.suppressErrorLogging) { + console['log'](e); + } + throw e; + } + return true; + }); + }; + } + }, + handleEx: function(cm, input) { + exCommandDispatcher.processCommand(cm, input); + }, + + defineMotion: defineMotion, + defineAction: defineAction, + defineOperator: defineOperator, + mapCommand: mapCommand, + _mapCommand: _mapCommand, + + defineRegister: defineRegister, + + exitVisualMode: exitVisualMode, + exitInsertMode: exitInsertMode + }; + + // Represents the current input state. + function InputState() { + this.prefixRepeat = []; + this.motionRepeat = []; + + this.operator = null; + this.operatorArgs = null; + this.motion = null; + this.motionArgs = null; + this.keyBuffer = []; // For matching multi-key commands. + this.registerName = null; // Defaults to the unnamed register. + } + InputState.prototype.pushRepeatDigit = function(n) { + if (!this.operator) { + this.prefixRepeat = this.prefixRepeat.concat(n); + } else { + this.motionRepeat = this.motionRepeat.concat(n); + } + }; + InputState.prototype.getRepeat = function() { + var repeat = 0; + if (this.prefixRepeat.length > 0 || this.motionRepeat.length > 0) { + repeat = 1; + if (this.prefixRepeat.length > 0) { + repeat *= parseInt(this.prefixRepeat.join(''), 10); + } + if (this.motionRepeat.length > 0) { + repeat *= parseInt(this.motionRepeat.join(''), 10); + } + } + return repeat; + }; + + function clearInputState(cm, reason) { + cm.state.vim.inputState = new InputState(); + CodeMirror.signal(cm, 'vim-command-done', reason); + } + + /* + * Register stores information about copy and paste registers. Besides + * text, a register must store whether it is linewise (i.e., when it is + * pasted, should it insert itself into a new line, or should the text be + * inserted at the cursor position.) + */ + function Register(text, linewise, blockwise) { + this.clear(); + this.keyBuffer = [text || '']; + this.insertModeChanges = []; + this.searchQueries = []; + this.linewise = !!linewise; + this.blockwise = !!blockwise; + } + Register.prototype = { + setText: function(text, linewise, blockwise) { + this.keyBuffer = [text || '']; + this.linewise = !!linewise; + this.blockwise = !!blockwise; + }, + pushText: function(text, linewise) { + // if this register has ever been set to linewise, use linewise. + if (linewise) { + if (!this.linewise) { + this.keyBuffer.push('\n'); + } + this.linewise = true; + } + this.keyBuffer.push(text); + }, + pushInsertModeChanges: function(changes) { + this.insertModeChanges.push(createInsertModeChanges(changes)); + }, + pushSearchQuery: function(query) { + this.searchQueries.push(query); + }, + clear: function() { + this.keyBuffer = []; + this.insertModeChanges = []; + this.searchQueries = []; + this.linewise = false; + }, + toString: function() { + return this.keyBuffer.join(''); + } + }; + + /** + * Defines an external register. + * + * The name should be a single character that will be used to reference the register. + * The register should support setText, pushText, clear, and toString(). See Register + * for a reference implementation. + */ + function defineRegister(name, register) { + var registers = vimGlobalState.registerController.registers[name]; + if (!name || name.length != 1) { + throw Error('Register name must be 1 character'); + } + if (registers[name]) { + throw Error('Register already defined ' + name); + } + registers[name] = register; + validRegisters.push(name); + } + + /* + * vim registers allow you to keep many independent copy and paste buffers. + * See http://usevim.com/2012/04/13/registers/ for an introduction. + * + * RegisterController keeps the state of all the registers. An initial + * state may be passed in. The unnamed register '"' will always be + * overridden. + */ + function RegisterController(registers) { + this.registers = registers; + this.unnamedRegister = registers['"'] = new Register(); + registers['.'] = new Register(); + registers[':'] = new Register(); + registers['/'] = new Register(); + } + RegisterController.prototype = { + pushText: function(registerName, operator, text, linewise, blockwise) { + if (linewise && text.charAt(0) == '\n') { + text = text.slice(1) + '\n'; + } + if (linewise && text.charAt(text.length - 1) !== '\n'){ + text += '\n'; + } + // Lowercase and uppercase registers refer to the same register. + // Uppercase just means append. + var register = this.isValidRegister(registerName) ? + this.getRegister(registerName) : null; + // if no register/an invalid register was specified, things go to the + // default registers + if (!register) { + switch (operator) { + case 'yank': + // The 0 register contains the text from the most recent yank. + this.registers['0'] = new Register(text, linewise, blockwise); + break; + case 'delete': + case 'change': + if (text.indexOf('\n') == -1) { + // Delete less than 1 line. Update the small delete register. + this.registers['-'] = new Register(text, linewise); + } else { + // Shift down the contents of the numbered registers and put the + // deleted text into register 1. + this.shiftNumericRegisters_(); + this.registers['1'] = new Register(text, linewise); + } + break; + } + // Make sure the unnamed register is set to what just happened + this.unnamedRegister.setText(text, linewise, blockwise); + return; + } + + // If we've gotten to this point, we've actually specified a register + var append = isUpperCase(registerName); + if (append) { + register.pushText(text, linewise); + } else { + register.setText(text, linewise, blockwise); + } + // The unnamed register always has the same value as the last used + // register. + this.unnamedRegister.setText(register.toString(), linewise); + }, + // Gets the register named @name. If one of @name doesn't already exist, + // create it. If @name is invalid, return the unnamedRegister. + getRegister: function(name) { + if (!this.isValidRegister(name)) { + return this.unnamedRegister; + } + name = name.toLowerCase(); + if (!this.registers[name]) { + this.registers[name] = new Register(); + } + return this.registers[name]; + }, + isValidRegister: function(name) { + return name && inArray(name, validRegisters); + }, + shiftNumericRegisters_: function() { + for (var i = 9; i >= 2; i--) { + this.registers[i] = this.getRegister('' + (i - 1)); + } + } + }; + function HistoryController() { + this.historyBuffer = []; + this.iterator = 0; + this.initialPrefix = null; + } + HistoryController.prototype = { + // the input argument here acts a user entered prefix for a small time + // until we start autocompletion in which case it is the autocompleted. + nextMatch: function (input, up) { + var historyBuffer = this.historyBuffer; + var dir = up ? -1 : 1; + if (this.initialPrefix === null) this.initialPrefix = input; + for (var i = this.iterator + dir; up ? i >= 0 : i < historyBuffer.length; i+= dir) { + var element = historyBuffer[i]; + for (var j = 0; j <= element.length; j++) { + if (this.initialPrefix == element.substring(0, j)) { + this.iterator = i; + return element; + } + } + } + // should return the user input in case we reach the end of buffer. + if (i >= historyBuffer.length) { + this.iterator = historyBuffer.length; + return this.initialPrefix; + } + // return the last autocompleted query or exCommand as it is. + if (i < 0 ) return input; + }, + pushInput: function(input) { + var index = this.historyBuffer.indexOf(input); + if (index > -1) this.historyBuffer.splice(index, 1); + if (input.length) this.historyBuffer.push(input); + }, + reset: function() { + this.initialPrefix = null; + this.iterator = this.historyBuffer.length; + } + }; + var commandDispatcher = { + matchCommand: function(keys, keyMap, inputState, context) { + var matches = commandMatches(keys, keyMap, context, inputState); + if (!matches.full && !matches.partial) { + return {type: 'none'}; + } else if (!matches.full && matches.partial) { + return {type: 'partial'}; + } + + var bestMatch; + for (var i = 0; i < matches.full.length; i++) { + var match = matches.full[i]; + if (!bestMatch) { + bestMatch = match; + } + } + if (bestMatch.keys.slice(-11) == '') { + inputState.selectedCharacter = lastChar(keys); + } + return {type: 'full', command: bestMatch}; + }, + processCommand: function(cm, vim, command) { + vim.inputState.repeatOverride = command.repeatOverride; + switch (command.type) { + case 'motion': + this.processMotion(cm, vim, command); + break; + case 'operator': + this.processOperator(cm, vim, command); + break; + case 'operatorMotion': + this.processOperatorMotion(cm, vim, command); + break; + case 'action': + this.processAction(cm, vim, command); + break; + case 'search': + this.processSearch(cm, vim, command); + break; + case 'ex': + case 'keyToEx': + this.processEx(cm, vim, command); + break; + default: + break; + } + }, + processMotion: function(cm, vim, command) { + vim.inputState.motion = command.motion; + vim.inputState.motionArgs = copyArgs(command.motionArgs); + this.evalInput(cm, vim); + }, + processOperator: function(cm, vim, command) { + var inputState = vim.inputState; + if (inputState.operator) { + if (inputState.operator == command.operator) { + // Typing an operator twice like 'dd' makes the operator operate + // linewise + inputState.motion = 'expandToLine'; + inputState.motionArgs = { linewise: true }; + this.evalInput(cm, vim); + return; + } else { + // 2 different operators in a row doesn't make sense. + clearInputState(cm); + } + } + inputState.operator = command.operator; + inputState.operatorArgs = copyArgs(command.operatorArgs); + if (vim.visualMode) { + // Operating on a selection in visual mode. We don't need a motion. + this.evalInput(cm, vim); + } + }, + processOperatorMotion: function(cm, vim, command) { + var visualMode = vim.visualMode; + var operatorMotionArgs = copyArgs(command.operatorMotionArgs); + if (operatorMotionArgs) { + // Operator motions may have special behavior in visual mode. + if (visualMode && operatorMotionArgs.visualLine) { + vim.visualLine = true; + } + } + this.processOperator(cm, vim, command); + if (!visualMode) { + this.processMotion(cm, vim, command); + } + }, + processAction: function(cm, vim, command) { + var inputState = vim.inputState; + var repeat = inputState.getRepeat(); + var repeatIsExplicit = !!repeat; + var actionArgs = copyArgs(command.actionArgs) || {}; + if (inputState.selectedCharacter) { + actionArgs.selectedCharacter = inputState.selectedCharacter; + } + // Actions may or may not have motions and operators. Do these first. + if (command.operator) { + this.processOperator(cm, vim, command); + } + if (command.motion) { + this.processMotion(cm, vim, command); + } + if (command.motion || command.operator) { + this.evalInput(cm, vim); + } + actionArgs.repeat = repeat || 1; + actionArgs.repeatIsExplicit = repeatIsExplicit; + actionArgs.registerName = inputState.registerName; + clearInputState(cm); + vim.lastMotion = null; + if (command.isEdit) { + this.recordLastEdit(vim, inputState, command); + } + actions[command.action](cm, actionArgs, vim); + }, + processSearch: function(cm, vim, command) { + if (!cm.getSearchCursor) { + // Search depends on SearchCursor. + return; + } + var forward = command.searchArgs.forward; + var wholeWordOnly = command.searchArgs.wholeWordOnly; + getSearchState(cm).setReversed(!forward); + var promptPrefix = (forward) ? '/' : '?'; + var originalQuery = getSearchState(cm).getQuery(); + var originalScrollPos = cm.getScrollInfo(); + function handleQuery(query, ignoreCase, smartCase) { + vimGlobalState.searchHistoryController.pushInput(query); + vimGlobalState.searchHistoryController.reset(); + try { + updateSearchQuery(cm, query, ignoreCase, smartCase); + } catch (e) { + showConfirm(cm, 'Invalid regex: ' + query); + clearInputState(cm); + return; + } + commandDispatcher.processMotion(cm, vim, { + type: 'motion', + motion: 'findNext', + motionArgs: { forward: true, toJumplist: command.searchArgs.toJumplist } + }); + } + function onPromptClose(query) { + cm.scrollTo(originalScrollPos.left, originalScrollPos.top); + handleQuery(query, true /** ignoreCase */, true /** smartCase */); + var macroModeState = vimGlobalState.macroModeState; + if (macroModeState.isRecording) { + logSearchQuery(macroModeState, query); + } + } + function onPromptKeyUp(e, query, close) { + var keyName = CodeMirror.keyName(e), up; + if (keyName == 'Up' || keyName == 'Down') { + up = keyName == 'Up' ? true : false; + query = vimGlobalState.searchHistoryController.nextMatch(query, up) || ''; + close(query); + } else { + if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift') + vimGlobalState.searchHistoryController.reset(); + } + var parsedQuery; + try { + parsedQuery = updateSearchQuery(cm, query, + true /** ignoreCase */, true /** smartCase */); + } catch (e) { + // Swallow bad regexes for incremental search. + } + if (parsedQuery) { + cm.scrollIntoView(findNext(cm, !forward, parsedQuery), 30); + } else { + clearSearchHighlight(cm); + cm.scrollTo(originalScrollPos.left, originalScrollPos.top); + } + } + function onPromptKeyDown(e, query, close) { + var keyName = CodeMirror.keyName(e); + if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[' || + (keyName == 'Backspace' && query == '')) { + vimGlobalState.searchHistoryController.pushInput(query); + vimGlobalState.searchHistoryController.reset(); + updateSearchQuery(cm, originalQuery); + clearSearchHighlight(cm); + cm.scrollTo(originalScrollPos.left, originalScrollPos.top); + CodeMirror.e_stop(e); + clearInputState(cm); + close(); + cm.focus(); + } else if (keyName == 'Ctrl-U') { + // Ctrl-U clears input. + CodeMirror.e_stop(e); + close(''); + } + } + switch (command.searchArgs.querySrc) { + case 'prompt': + var macroModeState = vimGlobalState.macroModeState; + if (macroModeState.isPlaying) { + var query = macroModeState.replaySearchQueries.shift(); + handleQuery(query, true /** ignoreCase */, false /** smartCase */); + } else { + showPrompt(cm, { + onClose: onPromptClose, + prefix: promptPrefix, + desc: searchPromptDesc, + onKeyUp: onPromptKeyUp, + onKeyDown: onPromptKeyDown + }); + } + break; + case 'wordUnderCursor': + var word = expandWordUnderCursor(cm, false /** inclusive */, + true /** forward */, false /** bigWord */, + true /** noSymbol */); + var isKeyword = true; + if (!word) { + word = expandWordUnderCursor(cm, false /** inclusive */, + true /** forward */, false /** bigWord */, + false /** noSymbol */); + isKeyword = false; + } + if (!word) { + return; + } + var query = cm.getLine(word.start.line).substring(word.start.ch, + word.end.ch); + if (isKeyword && wholeWordOnly) { + query = '\\b' + query + '\\b'; + } else { + query = escapeRegex(query); + } + + // cachedCursor is used to save the old position of the cursor + // when * or # causes vim to seek for the nearest word and shift + // the cursor before entering the motion. + vimGlobalState.jumpList.cachedCursor = cm.getCursor(); + cm.setCursor(word.start); + + handleQuery(query, true /** ignoreCase */, false /** smartCase */); + break; + } + }, + processEx: function(cm, vim, command) { + function onPromptClose(input) { + // Give the prompt some time to close so that if processCommand shows + // an error, the elements don't overlap. + vimGlobalState.exCommandHistoryController.pushInput(input); + vimGlobalState.exCommandHistoryController.reset(); + exCommandDispatcher.processCommand(cm, input); + } + function onPromptKeyDown(e, input, close) { + var keyName = CodeMirror.keyName(e), up; + if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[' || + (keyName == 'Backspace' && input == '')) { + vimGlobalState.exCommandHistoryController.pushInput(input); + vimGlobalState.exCommandHistoryController.reset(); + CodeMirror.e_stop(e); + clearInputState(cm); + close(); + cm.focus(); + } + if (keyName == 'Up' || keyName == 'Down') { + up = keyName == 'Up' ? true : false; + input = vimGlobalState.exCommandHistoryController.nextMatch(input, up) || ''; + close(input); + } else if (keyName == 'Ctrl-U') { + // Ctrl-U clears input. + CodeMirror.e_stop(e); + close(''); + } else { + if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift') + vimGlobalState.exCommandHistoryController.reset(); + } + } + if (command.type == 'keyToEx') { + // Handle user defined Ex to Ex mappings + exCommandDispatcher.processCommand(cm, command.exArgs.input); + } else { + if (vim.visualMode) { + showPrompt(cm, { onClose: onPromptClose, prefix: ':', value: '\'<,\'>', + onKeyDown: onPromptKeyDown}); + } else { + showPrompt(cm, { onClose: onPromptClose, prefix: ':', + onKeyDown: onPromptKeyDown}); + } + } + }, + evalInput: function(cm, vim) { + // If the motion command is set, execute both the operator and motion. + // Otherwise return. + var inputState = vim.inputState; + var motion = inputState.motion; + var motionArgs = inputState.motionArgs || {}; + var operator = inputState.operator; + var operatorArgs = inputState.operatorArgs || {}; + var registerName = inputState.registerName; + var sel = vim.sel; + // TODO: Make sure cm and vim selections are identical outside visual mode. + var origHead = copyCursor(vim.visualMode ? clipCursorToContent(cm, sel.head): cm.getCursor('head')); + var origAnchor = copyCursor(vim.visualMode ? clipCursorToContent(cm, sel.anchor) : cm.getCursor('anchor')); + var oldHead = copyCursor(origHead); + var oldAnchor = copyCursor(origAnchor); + var newHead, newAnchor; + var repeat; + if (operator) { + this.recordLastEdit(vim, inputState); + } + if (inputState.repeatOverride !== undefined) { + // If repeatOverride is specified, that takes precedence over the + // input state's repeat. Used by Ex mode and can be user defined. + repeat = inputState.repeatOverride; + } else { + repeat = inputState.getRepeat(); + } + if (repeat > 0 && motionArgs.explicitRepeat) { + motionArgs.repeatIsExplicit = true; + } else if (motionArgs.noRepeat || + (!motionArgs.explicitRepeat && repeat === 0)) { + repeat = 1; + motionArgs.repeatIsExplicit = false; + } + if (inputState.selectedCharacter) { + // If there is a character input, stick it in all of the arg arrays. + motionArgs.selectedCharacter = operatorArgs.selectedCharacter = + inputState.selectedCharacter; + } + motionArgs.repeat = repeat; + clearInputState(cm); + if (motion) { + var motionResult = motions[motion](cm, origHead, motionArgs, vim); + vim.lastMotion = motions[motion]; + if (!motionResult) { + return; + } + if (motionArgs.toJumplist) { + var jumpList = vimGlobalState.jumpList; + // if the current motion is # or *, use cachedCursor + var cachedCursor = jumpList.cachedCursor; + if (cachedCursor) { + recordJumpPosition(cm, cachedCursor, motionResult); + delete jumpList.cachedCursor; + } else { + recordJumpPosition(cm, origHead, motionResult); + } + } + if (motionResult instanceof Array) { + newAnchor = motionResult[0]; + newHead = motionResult[1]; + } else { + newHead = motionResult; + } + // TODO: Handle null returns from motion commands better. + if (!newHead) { + newHead = copyCursor(origHead); + } + if (vim.visualMode) { + if (!(vim.visualBlock && newHead.ch === Infinity)) { + newHead = clipCursorToContent(cm, newHead, vim.visualBlock); + } + if (newAnchor) { + newAnchor = clipCursorToContent(cm, newAnchor, true); + } + newAnchor = newAnchor || oldAnchor; + sel.anchor = newAnchor; + sel.head = newHead; + updateCmSelection(cm); + updateMark(cm, vim, '<', + cursorIsBefore(newAnchor, newHead) ? newAnchor + : newHead); + updateMark(cm, vim, '>', + cursorIsBefore(newAnchor, newHead) ? newHead + : newAnchor); + } else if (!operator) { + newHead = clipCursorToContent(cm, newHead); + cm.setCursor(newHead.line, newHead.ch); + } + } + if (operator) { + if (operatorArgs.lastSel) { + // Replaying a visual mode operation + newAnchor = oldAnchor; + var lastSel = operatorArgs.lastSel; + var lineOffset = Math.abs(lastSel.head.line - lastSel.anchor.line); + var chOffset = Math.abs(lastSel.head.ch - lastSel.anchor.ch); + if (lastSel.visualLine) { + // Linewise Visual mode: The same number of lines. + newHead = Pos(oldAnchor.line + lineOffset, oldAnchor.ch); + } else if (lastSel.visualBlock) { + // Blockwise Visual mode: The same number of lines and columns. + newHead = Pos(oldAnchor.line + lineOffset, oldAnchor.ch + chOffset); + } else if (lastSel.head.line == lastSel.anchor.line) { + // Normal Visual mode within one line: The same number of characters. + newHead = Pos(oldAnchor.line, oldAnchor.ch + chOffset); + } else { + // Normal Visual mode with several lines: The same number of lines, in the + // last line the same number of characters as in the last line the last time. + newHead = Pos(oldAnchor.line + lineOffset, oldAnchor.ch); + } + vim.visualMode = true; + vim.visualLine = lastSel.visualLine; + vim.visualBlock = lastSel.visualBlock; + sel = vim.sel = { + anchor: newAnchor, + head: newHead + }; + updateCmSelection(cm); + } else if (vim.visualMode) { + operatorArgs.lastSel = { + anchor: copyCursor(sel.anchor), + head: copyCursor(sel.head), + visualBlock: vim.visualBlock, + visualLine: vim.visualLine + }; + } + var curStart, curEnd, linewise, mode; + var cmSel; + if (vim.visualMode) { + // Init visual op + curStart = cursorMin(sel.head, sel.anchor); + curEnd = cursorMax(sel.head, sel.anchor); + linewise = vim.visualLine || operatorArgs.linewise; + mode = vim.visualBlock ? 'block' : + linewise ? 'line' : + 'char'; + cmSel = makeCmSelection(cm, { + anchor: curStart, + head: curEnd + }, mode); + if (linewise) { + var ranges = cmSel.ranges; + if (mode == 'block') { + // Linewise operators in visual block mode extend to end of line + for (var i = 0; i < ranges.length; i++) { + ranges[i].head.ch = lineLength(cm, ranges[i].head.line); + } + } else if (mode == 'line') { + ranges[0].head = Pos(ranges[0].head.line + 1, 0); + } + } + } else { + // Init motion op + curStart = copyCursor(newAnchor || oldAnchor); + curEnd = copyCursor(newHead || oldHead); + if (cursorIsBefore(curEnd, curStart)) { + var tmp = curStart; + curStart = curEnd; + curEnd = tmp; + } + linewise = motionArgs.linewise || operatorArgs.linewise; + if (linewise) { + // Expand selection to entire line. + expandSelectionToLine(cm, curStart, curEnd); + } else if (motionArgs.forward) { + // Clip to trailing newlines only if the motion goes forward. + clipToLine(cm, curStart, curEnd); + } + mode = 'char'; + var exclusive = !motionArgs.inclusive || linewise; + cmSel = makeCmSelection(cm, { + anchor: curStart, + head: curEnd + }, mode, exclusive); + } + cm.setSelections(cmSel.ranges, cmSel.primary); + vim.lastMotion = null; + operatorArgs.repeat = repeat; // For indent in visual mode. + operatorArgs.registerName = registerName; + // Keep track of linewise as it affects how paste and change behave. + operatorArgs.linewise = linewise; + var operatorMoveTo = operators[operator]( + cm, operatorArgs, cmSel.ranges, oldAnchor, newHead); + if (vim.visualMode) { + exitVisualMode(cm, operatorMoveTo != null); + } + if (operatorMoveTo) { + cm.setCursor(operatorMoveTo); + } + } + }, + recordLastEdit: function(vim, inputState, actionCommand) { + var macroModeState = vimGlobalState.macroModeState; + if (macroModeState.isPlaying) { return; } + vim.lastEditInputState = inputState; + vim.lastEditActionCommand = actionCommand; + macroModeState.lastInsertModeChanges.changes = []; + macroModeState.lastInsertModeChanges.expectCursorActivityForChange = false; + } + }; + + /** + * typedef {Object{line:number,ch:number}} Cursor An object containing the + * position of the cursor. + */ + // All of the functions below return Cursor objects. + var motions = { + moveToTopLine: function(cm, _head, motionArgs) { + var line = getUserVisibleLines(cm).top + motionArgs.repeat -1; + return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); + }, + moveToMiddleLine: function(cm) { + var range = getUserVisibleLines(cm); + var line = Math.floor((range.top + range.bottom) * 0.5); + return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); + }, + moveToBottomLine: function(cm, _head, motionArgs) { + var line = getUserVisibleLines(cm).bottom - motionArgs.repeat +1; + return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); + }, + expandToLine: function(_cm, head, motionArgs) { + // Expands forward to end of line, and then to next line if repeat is + // >1. Does not handle backward motion! + var cur = head; + return Pos(cur.line + motionArgs.repeat - 1, Infinity); + }, + findNext: function(cm, _head, motionArgs) { + var state = getSearchState(cm); + var query = state.getQuery(); + if (!query) { + return; + } + var prev = !motionArgs.forward; + // If search is initiated with ? instead of /, negate direction. + prev = (state.isReversed()) ? !prev : prev; + highlightSearchMatches(cm, query); + return findNext(cm, prev/** prev */, query, motionArgs.repeat); + }, + goToMark: function(cm, _head, motionArgs, vim) { + var mark = vim.marks[motionArgs.selectedCharacter]; + if (mark) { + var pos = mark.find(); + return motionArgs.linewise ? { line: pos.line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(pos.line)) } : pos; + } + return null; + }, + moveToOtherHighlightedEnd: function(cm, _head, motionArgs, vim) { + if (vim.visualBlock && motionArgs.sameLine) { + var sel = vim.sel; + return [ + clipCursorToContent(cm, Pos(sel.anchor.line, sel.head.ch)), + clipCursorToContent(cm, Pos(sel.head.line, sel.anchor.ch)) + ]; + } else { + return ([vim.sel.head, vim.sel.anchor]); + } + }, + jumpToMark: function(cm, head, motionArgs, vim) { + var best = head; + for (var i = 0; i < motionArgs.repeat; i++) { + var cursor = best; + for (var key in vim.marks) { + if (!isLowerCase(key)) { + continue; + } + var mark = vim.marks[key].find(); + var isWrongDirection = (motionArgs.forward) ? + cursorIsBefore(mark, cursor) : cursorIsBefore(cursor, mark); + + if (isWrongDirection) { + continue; + } + if (motionArgs.linewise && (mark.line == cursor.line)) { + continue; + } + + var equal = cursorEqual(cursor, best); + var between = (motionArgs.forward) ? + cursorIsBetween(cursor, mark, best) : + cursorIsBetween(best, mark, cursor); + + if (equal || between) { + best = mark; + } + } + } + + if (motionArgs.linewise) { + // Vim places the cursor on the first non-whitespace character of + // the line if there is one, else it places the cursor at the end + // of the line, regardless of whether a mark was found. + best = Pos(best.line, findFirstNonWhiteSpaceCharacter(cm.getLine(best.line))); + } + return best; + }, + moveByCharacters: function(_cm, head, motionArgs) { + var cur = head; + var repeat = motionArgs.repeat; + var ch = motionArgs.forward ? cur.ch + repeat : cur.ch - repeat; + return Pos(cur.line, ch); + }, + moveByLines: function(cm, head, motionArgs, vim) { + var cur = head; + var endCh = cur.ch; + // Depending what our last motion was, we may want to do different + // things. If our last motion was moving vertically, we want to + // preserve the HPos from our last horizontal move. If our last motion + // was going to the end of a line, moving vertically we should go to + // the end of the line, etc. + switch (vim.lastMotion) { + case this.moveByLines: + case this.moveByDisplayLines: + case this.moveByScroll: + case this.moveToColumn: + case this.moveToEol: + endCh = vim.lastHPos; + break; + default: + vim.lastHPos = endCh; + } + var repeat = motionArgs.repeat+(motionArgs.repeatOffset||0); + var line = motionArgs.forward ? cur.line + repeat : cur.line - repeat; + var first = cm.firstLine(); + var last = cm.lastLine(); + // Vim go to line begin or line end when cursor at first/last line and + // move to previous/next line is triggered. + if (line < first && cur.line == first){ + return this.moveToStartOfLine(cm, head, motionArgs, vim); + }else if (line > last && cur.line == last){ + return this.moveToEol(cm, head, motionArgs, vim); + } + if (motionArgs.toFirstChar){ + endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line)); + vim.lastHPos = endCh; + } + vim.lastHSPos = cm.charCoords(Pos(line, endCh),'div').left; + return Pos(line, endCh); + }, + moveByDisplayLines: function(cm, head, motionArgs, vim) { + var cur = head; + switch (vim.lastMotion) { + case this.moveByDisplayLines: + case this.moveByScroll: + case this.moveByLines: + case this.moveToColumn: + case this.moveToEol: + break; + default: + vim.lastHSPos = cm.charCoords(cur,'div').left; + } + var repeat = motionArgs.repeat; + var res=cm.findPosV(cur,(motionArgs.forward ? repeat : -repeat),'line',vim.lastHSPos); + if (res.hitSide) { + if (motionArgs.forward) { + var lastCharCoords = cm.charCoords(res, 'div'); + var goalCoords = { top: lastCharCoords.top + 8, left: vim.lastHSPos }; + var res = cm.coordsChar(goalCoords, 'div'); + } else { + var resCoords = cm.charCoords(Pos(cm.firstLine(), 0), 'div'); + resCoords.left = vim.lastHSPos; + res = cm.coordsChar(resCoords, 'div'); + } + } + vim.lastHPos = res.ch; + return res; + }, + moveByPage: function(cm, head, motionArgs) { + // CodeMirror only exposes functions that move the cursor page down, so + // doing this bad hack to move the cursor and move it back. evalInput + // will move the cursor to where it should be in the end. + var curStart = head; + var repeat = motionArgs.repeat; + return cm.findPosV(curStart, (motionArgs.forward ? repeat : -repeat), 'page'); + }, + moveByParagraph: function(cm, head, motionArgs) { + var dir = motionArgs.forward ? 1 : -1; + return findParagraph(cm, head, motionArgs.repeat, dir); + }, + moveByScroll: function(cm, head, motionArgs, vim) { + var scrollbox = cm.getScrollInfo(); + var curEnd = null; + var repeat = motionArgs.repeat; + if (!repeat) { + repeat = scrollbox.clientHeight / (2 * cm.defaultTextHeight()); + } + var orig = cm.charCoords(head, 'local'); + motionArgs.repeat = repeat; + var curEnd = motions.moveByDisplayLines(cm, head, motionArgs, vim); + if (!curEnd) { + return null; + } + var dest = cm.charCoords(curEnd, 'local'); + cm.scrollTo(null, scrollbox.top + dest.top - orig.top); + return curEnd; + }, + moveByWords: function(cm, head, motionArgs) { + return moveToWord(cm, head, motionArgs.repeat, !!motionArgs.forward, + !!motionArgs.wordEnd, !!motionArgs.bigWord); + }, + moveTillCharacter: function(cm, _head, motionArgs) { + var repeat = motionArgs.repeat; + var curEnd = moveToCharacter(cm, repeat, motionArgs.forward, + motionArgs.selectedCharacter); + var increment = motionArgs.forward ? -1 : 1; + recordLastCharacterSearch(increment, motionArgs); + if (!curEnd) return null; + curEnd.ch += increment; + return curEnd; + }, + moveToCharacter: function(cm, head, motionArgs) { + var repeat = motionArgs.repeat; + recordLastCharacterSearch(0, motionArgs); + return moveToCharacter(cm, repeat, motionArgs.forward, + motionArgs.selectedCharacter) || head; + }, + moveToSymbol: function(cm, head, motionArgs) { + var repeat = motionArgs.repeat; + return findSymbol(cm, repeat, motionArgs.forward, + motionArgs.selectedCharacter) || head; + }, + moveToColumn: function(cm, head, motionArgs, vim) { + var repeat = motionArgs.repeat; + // repeat is equivalent to which column we want to move to! + vim.lastHPos = repeat - 1; + vim.lastHSPos = cm.charCoords(head,'div').left; + return moveToColumn(cm, repeat); + }, + moveToEol: function(cm, head, motionArgs, vim) { + var cur = head; + vim.lastHPos = Infinity; + var retval= Pos(cur.line + motionArgs.repeat - 1, Infinity); + var end=cm.clipPos(retval); + end.ch--; + vim.lastHSPos = cm.charCoords(end,'div').left; + return retval; + }, + moveToFirstNonWhiteSpaceCharacter: function(cm, head) { + // Go to the start of the line where the text begins, or the end for + // whitespace-only lines + var cursor = head; + return Pos(cursor.line, + findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line))); + }, + moveToMatchedSymbol: function(cm, head) { + var cursor = head; + var line = cursor.line; + var ch = cursor.ch; + var lineText = cm.getLine(line); + var symbol; + do { + symbol = lineText.charAt(ch++); + if (symbol && isMatchableSymbol(symbol)) { + var style = cm.getTokenTypeAt(Pos(line, ch)); + if (style !== "string" && style !== "comment") { + break; + } + } + } while (symbol); + if (symbol) { + var matched = cm.findMatchingBracket(Pos(line, ch)); + return matched.to; + } else { + return cursor; + } + }, + moveToStartOfLine: function(_cm, head) { + return Pos(head.line, 0); + }, + moveToLineOrEdgeOfDocument: function(cm, _head, motionArgs) { + var lineNum = motionArgs.forward ? cm.lastLine() : cm.firstLine(); + if (motionArgs.repeatIsExplicit) { + lineNum = motionArgs.repeat - cm.getOption('firstLineNumber'); + } + return Pos(lineNum, + findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum))); + }, + textObjectManipulation: function(cm, head, motionArgs, vim) { + // TODO: lots of possible exceptions that can be thrown here. Try da( + // outside of a () block. + + // TODO: adding <> >< to this map doesn't work, presumably because + // they're operators + var mirroredPairs = {'(': ')', ')': '(', + '{': '}', '}': '{', + '[': ']', ']': '['}; + var selfPaired = {'\'': true, '"': true}; + + var character = motionArgs.selectedCharacter; + // 'b' refers to '()' block. + // 'B' refers to '{}' block. + if (character == 'b') { + character = '('; + } else if (character == 'B') { + character = '{'; + } + + // Inclusive is the difference between a and i + // TODO: Instead of using the additional text object map to perform text + // object operations, merge the map into the defaultKeyMap and use + // motionArgs to define behavior. Define separate entries for 'aw', + // 'iw', 'a[', 'i[', etc. + var inclusive = !motionArgs.textObjectInner; + + var tmp; + if (mirroredPairs[character]) { + tmp = selectCompanionObject(cm, head, character, inclusive); + } else if (selfPaired[character]) { + tmp = findBeginningAndEnd(cm, head, character, inclusive); + } else if (character === 'W') { + tmp = expandWordUnderCursor(cm, inclusive, true /** forward */, + true /** bigWord */); + } else if (character === 'w') { + tmp = expandWordUnderCursor(cm, inclusive, true /** forward */, + false /** bigWord */); + } else if (character === 'p') { + tmp = findParagraph(cm, head, motionArgs.repeat, 0, inclusive); + motionArgs.linewise = true; + if (vim.visualMode) { + if (!vim.visualLine) { vim.visualLine = true; } + } else { + var operatorArgs = vim.inputState.operatorArgs; + if (operatorArgs) { operatorArgs.linewise = true; } + tmp.end.line--; + } + } else { + // No text object defined for this, don't move. + return null; + } + + if (!cm.state.vim.visualMode) { + return [tmp.start, tmp.end]; + } else { + return expandSelection(cm, tmp.start, tmp.end); + } + }, + + repeatLastCharacterSearch: function(cm, head, motionArgs) { + var lastSearch = vimGlobalState.lastCharacterSearch; + var repeat = motionArgs.repeat; + var forward = motionArgs.forward === lastSearch.forward; + var increment = (lastSearch.increment ? 1 : 0) * (forward ? -1 : 1); + cm.moveH(-increment, 'char'); + motionArgs.inclusive = forward ? true : false; + var curEnd = moveToCharacter(cm, repeat, forward, lastSearch.selectedCharacter); + if (!curEnd) { + cm.moveH(increment, 'char'); + return head; + } + curEnd.ch += increment; + return curEnd; + } + }; + + function defineMotion(name, fn) { + motions[name] = fn; + } + + function fillArray(val, times) { + var arr = []; + for (var i = 0; i < times; i++) { + arr.push(val); + } + return arr; + } + /** + * An operator acts on a text selection. It receives the list of selections + * as input. The corresponding CodeMirror selection is guaranteed to + * match the input selection. + */ + var operators = { + change: function(cm, args, ranges) { + var finalHead, text; + var vim = cm.state.vim; + vimGlobalState.macroModeState.lastInsertModeChanges.inVisualBlock = vim.visualBlock; + if (!vim.visualMode) { + var anchor = ranges[0].anchor, + head = ranges[0].head; + text = cm.getRange(anchor, head); + var lastState = vim.lastEditInputState || {}; + if (lastState.motion == "moveByWords" && !isWhiteSpaceString(text)) { + // Exclude trailing whitespace if the range is not all whitespace. + var match = (/\s+$/).exec(text); + if (match && lastState.motionArgs && lastState.motionArgs.forward) { + head = offsetCursor(head, 0, - match[0].length); + text = text.slice(0, - match[0].length); + } + } + var prevLineEnd = new Pos(anchor.line - 1, Number.MAX_VALUE); + var wasLastLine = cm.firstLine() == cm.lastLine(); + if (head.line > cm.lastLine() && args.linewise && !wasLastLine) { + cm.replaceRange('', prevLineEnd, head); + } else { + cm.replaceRange('', anchor, head); + } + if (args.linewise) { + // Push the next line back down, if there is a next line. + if (!wasLastLine) { + cm.setCursor(prevLineEnd); + CodeMirror.commands.newlineAndIndent(cm); + } + // make sure cursor ends up at the end of the line. + anchor.ch = Number.MAX_VALUE; + } + finalHead = anchor; + } else { + text = cm.getSelection(); + var replacement = fillArray('', ranges.length); + cm.replaceSelections(replacement); + finalHead = cursorMin(ranges[0].head, ranges[0].anchor); + } + vimGlobalState.registerController.pushText( + args.registerName, 'change', text, + args.linewise, ranges.length > 1); + actions.enterInsertMode(cm, {head: finalHead}, cm.state.vim); + }, + // delete is a javascript keyword. + 'delete': function(cm, args, ranges) { + var finalHead, text; + var vim = cm.state.vim; + if (!vim.visualBlock) { + var anchor = ranges[0].anchor, + head = ranges[0].head; + if (args.linewise && + head.line != cm.firstLine() && + anchor.line == cm.lastLine() && + anchor.line == head.line - 1) { + // Special case for dd on last line (and first line). + if (anchor.line == cm.firstLine()) { + anchor.ch = 0; + } else { + anchor = Pos(anchor.line - 1, lineLength(cm, anchor.line - 1)); + } + } + text = cm.getRange(anchor, head); + cm.replaceRange('', anchor, head); + finalHead = anchor; + if (args.linewise) { + finalHead = motions.moveToFirstNonWhiteSpaceCharacter(cm, anchor); + } + } else { + text = cm.getSelection(); + var replacement = fillArray('', ranges.length); + cm.replaceSelections(replacement); + finalHead = ranges[0].anchor; + } + vimGlobalState.registerController.pushText( + args.registerName, 'delete', text, + args.linewise, vim.visualBlock); + return clipCursorToContent(cm, finalHead); + }, + indent: function(cm, args, ranges) { + var vim = cm.state.vim; + var startLine = ranges[0].anchor.line; + var endLine = vim.visualBlock ? + ranges[ranges.length - 1].anchor.line : + ranges[0].head.line; + // In visual mode, n> shifts the selection right n times, instead of + // shifting n lines right once. + var repeat = (vim.visualMode) ? args.repeat : 1; + if (args.linewise) { + // The only way to delete a newline is to delete until the start of + // the next line, so in linewise mode evalInput will include the next + // line. We don't want this in indent, so we go back a line. + endLine--; + } + for (var i = startLine; i <= endLine; i++) { + for (var j = 0; j < repeat; j++) { + cm.indentLine(i, args.indentRight); + } + } + return motions.moveToFirstNonWhiteSpaceCharacter(cm, ranges[0].anchor); + }, + changeCase: function(cm, args, ranges, oldAnchor, newHead) { + var selections = cm.getSelections(); + var swapped = []; + var toLower = args.toLower; + for (var j = 0; j < selections.length; j++) { + var toSwap = selections[j]; + var text = ''; + if (toLower === true) { + text = toSwap.toLowerCase(); + } else if (toLower === false) { + text = toSwap.toUpperCase(); + } else { + for (var i = 0; i < toSwap.length; i++) { + var character = toSwap.charAt(i); + text += isUpperCase(character) ? character.toLowerCase() : + character.toUpperCase(); + } + } + swapped.push(text); + } + cm.replaceSelections(swapped); + if (args.shouldMoveCursor){ + return newHead; + } else if (!cm.state.vim.visualMode && args.linewise && ranges[0].anchor.line + 1 == ranges[0].head.line) { + return motions.moveToFirstNonWhiteSpaceCharacter(cm, oldAnchor); + } else if (args.linewise){ + return oldAnchor; + } else { + return cursorMin(ranges[0].anchor, ranges[0].head); + } + }, + yank: function(cm, args, ranges, oldAnchor) { + var vim = cm.state.vim; + var text = cm.getSelection(); + var endPos = vim.visualMode + ? cursorMin(vim.sel.anchor, vim.sel.head, ranges[0].head, ranges[0].anchor) + : oldAnchor; + vimGlobalState.registerController.pushText( + args.registerName, 'yank', + text, args.linewise, vim.visualBlock); + return endPos; + } + }; + + function defineOperator(name, fn) { + operators[name] = fn; + } + + var actions = { + jumpListWalk: function(cm, actionArgs, vim) { + if (vim.visualMode) { + return; + } + var repeat = actionArgs.repeat; + var forward = actionArgs.forward; + var jumpList = vimGlobalState.jumpList; + + var mark = jumpList.move(cm, forward ? repeat : -repeat); + var markPos = mark ? mark.find() : undefined; + markPos = markPos ? markPos : cm.getCursor(); + cm.setCursor(markPos); + }, + scroll: function(cm, actionArgs, vim) { + if (vim.visualMode) { + return; + } + var repeat = actionArgs.repeat || 1; + var lineHeight = cm.defaultTextHeight(); + var top = cm.getScrollInfo().top; + var delta = lineHeight * repeat; + var newPos = actionArgs.forward ? top + delta : top - delta; + var cursor = copyCursor(cm.getCursor()); + var cursorCoords = cm.charCoords(cursor, 'local'); + if (actionArgs.forward) { + if (newPos > cursorCoords.top) { + cursor.line += (newPos - cursorCoords.top) / lineHeight; + cursor.line = Math.ceil(cursor.line); + cm.setCursor(cursor); + cursorCoords = cm.charCoords(cursor, 'local'); + cm.scrollTo(null, cursorCoords.top); + } else { + // Cursor stays within bounds. Just reposition the scroll window. + cm.scrollTo(null, newPos); + } + } else { + var newBottom = newPos + cm.getScrollInfo().clientHeight; + if (newBottom < cursorCoords.bottom) { + cursor.line -= (cursorCoords.bottom - newBottom) / lineHeight; + cursor.line = Math.floor(cursor.line); + cm.setCursor(cursor); + cursorCoords = cm.charCoords(cursor, 'local'); + cm.scrollTo( + null, cursorCoords.bottom - cm.getScrollInfo().clientHeight); + } else { + // Cursor stays within bounds. Just reposition the scroll window. + cm.scrollTo(null, newPos); + } + } + }, + scrollToCursor: function(cm, actionArgs) { + var lineNum = cm.getCursor().line; + var charCoords = cm.charCoords(Pos(lineNum, 0), 'local'); + var height = cm.getScrollInfo().clientHeight; + var y = charCoords.top; + var lineHeight = charCoords.bottom - y; + switch (actionArgs.position) { + case 'center': y = y - (height / 2) + lineHeight; + break; + case 'bottom': y = y - height + lineHeight; + break; + } + cm.scrollTo(null, y); + }, + replayMacro: function(cm, actionArgs, vim) { + var registerName = actionArgs.selectedCharacter; + var repeat = actionArgs.repeat; + var macroModeState = vimGlobalState.macroModeState; + if (registerName == '@') { + registerName = macroModeState.latestRegister; + } + while(repeat--){ + executeMacroRegister(cm, vim, macroModeState, registerName); + } + }, + enterMacroRecordMode: function(cm, actionArgs) { + var macroModeState = vimGlobalState.macroModeState; + var registerName = actionArgs.selectedCharacter; + macroModeState.enterMacroRecordMode(cm, registerName); + }, + toggleOverwrite: function(cm) { + if (!cm.state.overwrite) { + cm.toggleOverwrite(true); + cm.setOption('keyMap', 'vim-replace'); + CodeMirror.signal(cm, "vim-mode-change", {mode: "replace"}); + } else { + cm.toggleOverwrite(false); + cm.setOption('keyMap', 'vim-insert'); + CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"}); + } + }, + enterInsertMode: function(cm, actionArgs, vim) { + if (cm.getOption('readOnly')) { return; } + vim.insertMode = true; + vim.insertModeRepeat = actionArgs && actionArgs.repeat || 1; + var insertAt = (actionArgs) ? actionArgs.insertAt : null; + var sel = vim.sel; + var head = actionArgs.head || cm.getCursor('head'); + var height = cm.listSelections().length; + if (insertAt == 'eol') { + head = Pos(head.line, lineLength(cm, head.line)); + } else if (insertAt == 'charAfter') { + head = offsetCursor(head, 0, 1); + } else if (insertAt == 'firstNonBlank') { + head = motions.moveToFirstNonWhiteSpaceCharacter(cm, head); + } else if (insertAt == 'startOfSelectedArea') { + if (!vim.visualBlock) { + if (sel.head.line < sel.anchor.line) { + head = sel.head; + } else { + head = Pos(sel.anchor.line, 0); + } + } else { + head = Pos( + Math.min(sel.head.line, sel.anchor.line), + Math.min(sel.head.ch, sel.anchor.ch)); + height = Math.abs(sel.head.line - sel.anchor.line) + 1; + } + } else if (insertAt == 'endOfSelectedArea') { + if (!vim.visualBlock) { + if (sel.head.line >= sel.anchor.line) { + head = offsetCursor(sel.head, 0, 1); + } else { + head = Pos(sel.anchor.line, 0); + } + } else { + head = Pos( + Math.min(sel.head.line, sel.anchor.line), + Math.max(sel.head.ch + 1, sel.anchor.ch)); + height = Math.abs(sel.head.line - sel.anchor.line) + 1; + } + } else if (insertAt == 'inplace') { + if (vim.visualMode){ + return; + } + } + cm.setOption('disableInput', false); + if (actionArgs && actionArgs.replace) { + // Handle Replace-mode as a special case of insert mode. + cm.toggleOverwrite(true); + cm.setOption('keyMap', 'vim-replace'); + CodeMirror.signal(cm, "vim-mode-change", {mode: "replace"}); + } else { + cm.toggleOverwrite(false); + cm.setOption('keyMap', 'vim-insert'); + CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"}); + } + if (!vimGlobalState.macroModeState.isPlaying) { + // Only record if not replaying. + cm.on('change', onChange); + CodeMirror.on(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown); + } + if (vim.visualMode) { + exitVisualMode(cm); + } + selectForInsert(cm, head, height); + }, + toggleVisualMode: function(cm, actionArgs, vim) { + var repeat = actionArgs.repeat; + var anchor = cm.getCursor(); + var head; + // TODO: The repeat should actually select number of characters/lines + // equal to the repeat times the size of the previous visual + // operation. + if (!vim.visualMode) { + // Entering visual mode + vim.visualMode = true; + vim.visualLine = !!actionArgs.linewise; + vim.visualBlock = !!actionArgs.blockwise; + head = clipCursorToContent( + cm, Pos(anchor.line, anchor.ch + repeat - 1), + true /** includeLineBreak */); + vim.sel = { + anchor: anchor, + head: head + }; + CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : ""}); + updateCmSelection(cm); + updateMark(cm, vim, '<', cursorMin(anchor, head)); + updateMark(cm, vim, '>', cursorMax(anchor, head)); + } else if (vim.visualLine ^ actionArgs.linewise || + vim.visualBlock ^ actionArgs.blockwise) { + // Toggling between modes + vim.visualLine = !!actionArgs.linewise; + vim.visualBlock = !!actionArgs.blockwise; + CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : ""}); + updateCmSelection(cm); + } else { + exitVisualMode(cm); + } + }, + reselectLastSelection: function(cm, _actionArgs, vim) { + var lastSelection = vim.lastSelection; + if (vim.visualMode) { + updateLastSelection(cm, vim); + } + if (lastSelection) { + var anchor = lastSelection.anchorMark.find(); + var head = lastSelection.headMark.find(); + if (!anchor || !head) { + // If the marks have been destroyed due to edits, do nothing. + return; + } + vim.sel = { + anchor: anchor, + head: head + }; + vim.visualMode = true; + vim.visualLine = lastSelection.visualLine; + vim.visualBlock = lastSelection.visualBlock; + updateCmSelection(cm); + updateMark(cm, vim, '<', cursorMin(anchor, head)); + updateMark(cm, vim, '>', cursorMax(anchor, head)); + CodeMirror.signal(cm, 'vim-mode-change', { + mode: 'visual', + subMode: vim.visualLine ? 'linewise' : + vim.visualBlock ? 'blockwise' : ''}); + } + }, + joinLines: function(cm, actionArgs, vim) { + var curStart, curEnd; + if (vim.visualMode) { + curStart = cm.getCursor('anchor'); + curEnd = cm.getCursor('head'); + if (cursorIsBefore(curEnd, curStart)) { + var tmp = curEnd; + curEnd = curStart; + curStart = tmp; + } + curEnd.ch = lineLength(cm, curEnd.line) - 1; + } else { + // Repeat is the number of lines to join. Minimum 2 lines. + var repeat = Math.max(actionArgs.repeat, 2); + curStart = cm.getCursor(); + curEnd = clipCursorToContent(cm, Pos(curStart.line + repeat - 1, + Infinity)); + } + var finalCh = 0; + for (var i = curStart.line; i < curEnd.line; i++) { + finalCh = lineLength(cm, curStart.line); + var tmp = Pos(curStart.line + 1, + lineLength(cm, curStart.line + 1)); + var text = cm.getRange(curStart, tmp); + text = text.replace(/\n\s*/g, ' '); + cm.replaceRange(text, curStart, tmp); + } + var curFinalPos = Pos(curStart.line, finalCh); + if (vim.visualMode) { + exitVisualMode(cm, false); + } + cm.setCursor(curFinalPos); + }, + newLineAndEnterInsertMode: function(cm, actionArgs, vim) { + vim.insertMode = true; + var insertAt = copyCursor(cm.getCursor()); + if (insertAt.line === cm.firstLine() && !actionArgs.after) { + // Special case for inserting newline before start of document. + cm.replaceRange('\n', Pos(cm.firstLine(), 0)); + cm.setCursor(cm.firstLine(), 0); + } else { + insertAt.line = (actionArgs.after) ? insertAt.line : + insertAt.line - 1; + insertAt.ch = lineLength(cm, insertAt.line); + cm.setCursor(insertAt); + var newlineFn = CodeMirror.commands.newlineAndIndentContinueComment || + CodeMirror.commands.newlineAndIndent; + newlineFn(cm); + } + this.enterInsertMode(cm, { repeat: actionArgs.repeat }, vim); + }, + paste: function(cm, actionArgs, vim) { + var cur = copyCursor(cm.getCursor()); + var register = vimGlobalState.registerController.getRegister( + actionArgs.registerName); + var text = register.toString(); + if (!text) { + return; + } + if (actionArgs.matchIndent) { + var tabSize = cm.getOption("tabSize"); + // length that considers tabs and tabSize + var whitespaceLength = function(str) { + var tabs = (str.split("\t").length - 1); + var spaces = (str.split(" ").length - 1); + return tabs * tabSize + spaces * 1; + }; + var currentLine = cm.getLine(cm.getCursor().line); + var indent = whitespaceLength(currentLine.match(/^\s*/)[0]); + // chomp last newline b/c don't want it to match /^\s*/gm + var chompedText = text.replace(/\n$/, ''); + var wasChomped = text !== chompedText; + var firstIndent = whitespaceLength(text.match(/^\s*/)[0]); + var text = chompedText.replace(/^\s*/gm, function(wspace) { + var newIndent = indent + (whitespaceLength(wspace) - firstIndent); + if (newIndent < 0) { + return ""; + } + else if (cm.getOption("indentWithTabs")) { + var quotient = Math.floor(newIndent / tabSize); + return Array(quotient + 1).join('\t'); + } + else { + return Array(newIndent + 1).join(' '); + } + }); + text += wasChomped ? "\n" : ""; + } + if (actionArgs.repeat > 1) { + var text = Array(actionArgs.repeat + 1).join(text); + } + var linewise = register.linewise; + var blockwise = register.blockwise; + if (linewise) { + if(vim.visualMode) { + text = vim.visualLine ? text.slice(0, -1) : '\n' + text.slice(0, text.length - 1) + '\n'; + } else if (actionArgs.after) { + // Move the newline at the end to the start instead, and paste just + // before the newline character of the line we are on right now. + text = '\n' + text.slice(0, text.length - 1); + cur.ch = lineLength(cm, cur.line); + } else { + cur.ch = 0; + } + } else { + if (blockwise) { + text = text.split('\n'); + for (var i = 0; i < text.length; i++) { + text[i] = (text[i] == '') ? ' ' : text[i]; + } + } + cur.ch += actionArgs.after ? 1 : 0; + } + var curPosFinal; + var idx; + if (vim.visualMode) { + // save the pasted text for reselection if the need arises + vim.lastPastedText = text; + var lastSelectionCurEnd; + var selectedArea = getSelectedAreaRange(cm, vim); + var selectionStart = selectedArea[0]; + var selectionEnd = selectedArea[1]; + var selectedText = cm.getSelection(); + var selections = cm.listSelections(); + var emptyStrings = new Array(selections.length).join('1').split('1'); + // save the curEnd marker before it get cleared due to cm.replaceRange. + if (vim.lastSelection) { + lastSelectionCurEnd = vim.lastSelection.headMark.find(); + } + // push the previously selected text to unnamed register + vimGlobalState.registerController.unnamedRegister.setText(selectedText); + if (blockwise) { + // first delete the selected text + cm.replaceSelections(emptyStrings); + // Set new selections as per the block length of the yanked text + selectionEnd = Pos(selectionStart.line + text.length-1, selectionStart.ch); + cm.setCursor(selectionStart); + selectBlock(cm, selectionEnd); + cm.replaceSelections(text); + curPosFinal = selectionStart; + } else if (vim.visualBlock) { + cm.replaceSelections(emptyStrings); + cm.setCursor(selectionStart); + cm.replaceRange(text, selectionStart, selectionStart); + curPosFinal = selectionStart; + } else { + cm.replaceRange(text, selectionStart, selectionEnd); + curPosFinal = cm.posFromIndex(cm.indexFromPos(selectionStart) + text.length - 1); + } + // restore the the curEnd marker + if(lastSelectionCurEnd) { + vim.lastSelection.headMark = cm.setBookmark(lastSelectionCurEnd); + } + if (linewise) { + curPosFinal.ch=0; + } + } else { + if (blockwise) { + cm.setCursor(cur); + for (var i = 0; i < text.length; i++) { + var line = cur.line+i; + if (line > cm.lastLine()) { + cm.replaceRange('\n', Pos(line, 0)); + } + var lastCh = lineLength(cm, line); + if (lastCh < cur.ch) { + extendLineToColumn(cm, line, cur.ch); + } + } + cm.setCursor(cur); + selectBlock(cm, Pos(cur.line + text.length-1, cur.ch)); + cm.replaceSelections(text); + curPosFinal = cur; + } else { + cm.replaceRange(text, cur); + // Now fine tune the cursor to where we want it. + if (linewise && actionArgs.after) { + curPosFinal = Pos( + cur.line + 1, + findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line + 1))); + } else if (linewise && !actionArgs.after) { + curPosFinal = Pos( + cur.line, + findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line))); + } else if (!linewise && actionArgs.after) { + idx = cm.indexFromPos(cur); + curPosFinal = cm.posFromIndex(idx + text.length - 1); + } else { + idx = cm.indexFromPos(cur); + curPosFinal = cm.posFromIndex(idx + text.length); + } + } + } + if (vim.visualMode) { + exitVisualMode(cm, false); + } + cm.setCursor(curPosFinal); + }, + undo: function(cm, actionArgs) { + cm.operation(function() { + repeatFn(cm, CodeMirror.commands.undo, actionArgs.repeat)(); + cm.setCursor(cm.getCursor('anchor')); + }); + }, + redo: function(cm, actionArgs) { + repeatFn(cm, CodeMirror.commands.redo, actionArgs.repeat)(); + }, + setRegister: function(_cm, actionArgs, vim) { + vim.inputState.registerName = actionArgs.selectedCharacter; + }, + setMark: function(cm, actionArgs, vim) { + var markName = actionArgs.selectedCharacter; + updateMark(cm, vim, markName, cm.getCursor()); + }, + replace: function(cm, actionArgs, vim) { + var replaceWith = actionArgs.selectedCharacter; + var curStart = cm.getCursor(); + var replaceTo; + var curEnd; + var selections = cm.listSelections(); + if (vim.visualMode) { + curStart = cm.getCursor('start'); + curEnd = cm.getCursor('end'); + } else { + var line = cm.getLine(curStart.line); + replaceTo = curStart.ch + actionArgs.repeat; + if (replaceTo > line.length) { + replaceTo=line.length; + } + curEnd = Pos(curStart.line, replaceTo); + } + if (replaceWith=='\n') { + if (!vim.visualMode) cm.replaceRange('', curStart, curEnd); + // special case, where vim help says to replace by just one line-break + (CodeMirror.commands.newlineAndIndentContinueComment || CodeMirror.commands.newlineAndIndent)(cm); + } else { + var replaceWithStr = cm.getRange(curStart, curEnd); + //replace all characters in range by selected, but keep linebreaks + replaceWithStr = replaceWithStr.replace(/[^\n]/g, replaceWith); + if (vim.visualBlock) { + // Tabs are split in visua block before replacing + var spaces = new Array(cm.getOption("tabSize")+1).join(' '); + replaceWithStr = cm.getSelection(); + replaceWithStr = replaceWithStr.replace(/\t/g, spaces).replace(/[^\n]/g, replaceWith).split('\n'); + cm.replaceSelections(replaceWithStr); + } else { + cm.replaceRange(replaceWithStr, curStart, curEnd); + } + if (vim.visualMode) { + curStart = cursorIsBefore(selections[0].anchor, selections[0].head) ? + selections[0].anchor : selections[0].head; + cm.setCursor(curStart); + exitVisualMode(cm, false); + } else { + cm.setCursor(offsetCursor(curEnd, 0, -1)); + } + } + }, + incrementNumberToken: function(cm, actionArgs) { + var cur = cm.getCursor(); + var lineStr = cm.getLine(cur.line); + var re = /-?\d+/g; + var match; + var start; + var end; + var numberStr; + var token; + while ((match = re.exec(lineStr)) !== null) { + token = match[0]; + start = match.index; + end = start + token.length; + if (cur.ch < end)break; + } + if (!actionArgs.backtrack && (end <= cur.ch))return; + if (token) { + var increment = actionArgs.increase ? 1 : -1; + var number = parseInt(token) + (increment * actionArgs.repeat); + var from = Pos(cur.line, start); + var to = Pos(cur.line, end); + numberStr = number.toString(); + cm.replaceRange(numberStr, from, to); + } else { + return; + } + cm.setCursor(Pos(cur.line, start + numberStr.length - 1)); + }, + repeatLastEdit: function(cm, actionArgs, vim) { + var lastEditInputState = vim.lastEditInputState; + if (!lastEditInputState) { return; } + var repeat = actionArgs.repeat; + if (repeat && actionArgs.repeatIsExplicit) { + vim.lastEditInputState.repeatOverride = repeat; + } else { + repeat = vim.lastEditInputState.repeatOverride || repeat; + } + repeatLastEdit(cm, vim, repeat, false /** repeatForInsert */); + }, + exitInsertMode: exitInsertMode + }; + + function defineAction(name, fn) { + actions[name] = fn; + } + + /* + * Below are miscellaneous utility functions used by vim.js + */ + + /** + * Clips cursor to ensure that line is within the buffer's range + * If includeLineBreak is true, then allow cur.ch == lineLength. + */ + function clipCursorToContent(cm, cur, includeLineBreak) { + var line = Math.min(Math.max(cm.firstLine(), cur.line), cm.lastLine() ); + var maxCh = lineLength(cm, line) - 1; + maxCh = (includeLineBreak) ? maxCh + 1 : maxCh; + var ch = Math.min(Math.max(0, cur.ch), maxCh); + return Pos(line, ch); + } + function copyArgs(args) { + var ret = {}; + for (var prop in args) { + if (args.hasOwnProperty(prop)) { + ret[prop] = args[prop]; + } + } + return ret; + } + function offsetCursor(cur, offsetLine, offsetCh) { + if (typeof offsetLine === 'object') { + offsetCh = offsetLine.ch; + offsetLine = offsetLine.line; + } + return Pos(cur.line + offsetLine, cur.ch + offsetCh); + } + function getOffset(anchor, head) { + return { + line: head.line - anchor.line, + ch: head.line - anchor.line + }; + } + function commandMatches(keys, keyMap, context, inputState) { + // Partial matches are not applied. They inform the key handler + // that the current key sequence is a subsequence of a valid key + // sequence, so that the key buffer is not cleared. + var match, partial = [], full = []; + for (var i = 0; i < keyMap.length; i++) { + var command = keyMap[i]; + if (context == 'insert' && command.context != 'insert' || + command.context && command.context != context || + inputState.operator && command.type == 'action' || + !(match = commandMatch(keys, command.keys))) { continue; } + if (match == 'partial') { partial.push(command); } + if (match == 'full') { full.push(command); } + } + return { + partial: partial.length && partial, + full: full.length && full + }; + } + function commandMatch(pressed, mapped) { + if (mapped.slice(-11) == '') { + // Last character matches anything. + var prefixLen = mapped.length - 11; + var pressedPrefix = pressed.slice(0, prefixLen); + var mappedPrefix = mapped.slice(0, prefixLen); + return pressedPrefix == mappedPrefix && pressed.length > prefixLen ? 'full' : + mappedPrefix.indexOf(pressedPrefix) == 0 ? 'partial' : false; + } else { + return pressed == mapped ? 'full' : + mapped.indexOf(pressed) == 0 ? 'partial' : false; + } + } + function lastChar(keys) { + var match = /^.*(<[\w\-]+>)$/.exec(keys); + var selectedCharacter = match ? match[1] : keys.slice(-1); + if (selectedCharacter.length > 1){ + switch(selectedCharacter){ + case '': + selectedCharacter='\n'; + break; + case '': + selectedCharacter=' '; + break; + default: + break; + } + } + return selectedCharacter; + } + function repeatFn(cm, fn, repeat) { + return function() { + for (var i = 0; i < repeat; i++) { + fn(cm); + } + }; + } + function copyCursor(cur) { + return Pos(cur.line, cur.ch); + } + function cursorEqual(cur1, cur2) { + return cur1.ch == cur2.ch && cur1.line == cur2.line; + } + function cursorIsBefore(cur1, cur2) { + if (cur1.line < cur2.line) { + return true; + } + if (cur1.line == cur2.line && cur1.ch < cur2.ch) { + return true; + } + return false; + } + function cursorMin(cur1, cur2) { + if (arguments.length > 2) { + cur2 = cursorMin.apply(undefined, Array.prototype.slice.call(arguments, 1)); + } + return cursorIsBefore(cur1, cur2) ? cur1 : cur2; + } + function cursorMax(cur1, cur2) { + if (arguments.length > 2) { + cur2 = cursorMax.apply(undefined, Array.prototype.slice.call(arguments, 1)); + } + return cursorIsBefore(cur1, cur2) ? cur2 : cur1; + } + function cursorIsBetween(cur1, cur2, cur3) { + // returns true if cur2 is between cur1 and cur3. + var cur1before2 = cursorIsBefore(cur1, cur2); + var cur2before3 = cursorIsBefore(cur2, cur3); + return cur1before2 && cur2before3; + } + function lineLength(cm, lineNum) { + return cm.getLine(lineNum).length; + } + function trim(s) { + if (s.trim) { + return s.trim(); + } + return s.replace(/^\s+|\s+$/g, ''); + } + function escapeRegex(s) { + return s.replace(/([.?*+$\[\]\/\\(){}|\-])/g, '\\$1'); + } + function extendLineToColumn(cm, lineNum, column) { + var endCh = lineLength(cm, lineNum); + var spaces = new Array(column-endCh+1).join(' '); + cm.setCursor(Pos(lineNum, endCh)); + cm.replaceRange(spaces, cm.getCursor()); + } + // This functions selects a rectangular block + // of text with selectionEnd as any of its corner + // Height of block: + // Difference in selectionEnd.line and first/last selection.line + // Width of the block: + // Distance between selectionEnd.ch and any(first considered here) selection.ch + function selectBlock(cm, selectionEnd) { + var selections = [], ranges = cm.listSelections(); + var head = copyCursor(cm.clipPos(selectionEnd)); + var isClipped = !cursorEqual(selectionEnd, head); + var curHead = cm.getCursor('head'); + var primIndex = getIndex(ranges, curHead); + var wasClipped = cursorEqual(ranges[primIndex].head, ranges[primIndex].anchor); + var max = ranges.length - 1; + var index = max - primIndex > primIndex ? max : 0; + var base = ranges[index].anchor; + + var firstLine = Math.min(base.line, head.line); + var lastLine = Math.max(base.line, head.line); + var baseCh = base.ch, headCh = head.ch; + + var dir = ranges[index].head.ch - baseCh; + var newDir = headCh - baseCh; + if (dir > 0 && newDir <= 0) { + baseCh++; + if (!isClipped) { headCh--; } + } else if (dir < 0 && newDir >= 0) { + baseCh--; + if (!wasClipped) { headCh++; } + } else if (dir < 0 && newDir == -1) { + baseCh--; + headCh++; + } + for (var line = firstLine; line <= lastLine; line++) { + var range = {anchor: new Pos(line, baseCh), head: new Pos(line, headCh)}; + selections.push(range); + } + primIndex = head.line == lastLine ? selections.length - 1 : 0; + cm.setSelections(selections); + selectionEnd.ch = headCh; + base.ch = baseCh; + return base; + } + function selectForInsert(cm, head, height) { + var sel = []; + for (var i = 0; i < height; i++) { + var lineHead = offsetCursor(head, i, 0); + sel.push({anchor: lineHead, head: lineHead}); + } + cm.setSelections(sel, 0); + } + // getIndex returns the index of the cursor in the selections. + function getIndex(ranges, cursor, end) { + for (var i = 0; i < ranges.length; i++) { + var atAnchor = end != 'head' && cursorEqual(ranges[i].anchor, cursor); + var atHead = end != 'anchor' && cursorEqual(ranges[i].head, cursor); + if (atAnchor || atHead) { + return i; + } + } + return -1; + } + function getSelectedAreaRange(cm, vim) { + var lastSelection = vim.lastSelection; + var getCurrentSelectedAreaRange = function() { + var selections = cm.listSelections(); + var start = selections[0]; + var end = selections[selections.length-1]; + var selectionStart = cursorIsBefore(start.anchor, start.head) ? start.anchor : start.head; + var selectionEnd = cursorIsBefore(end.anchor, end.head) ? end.head : end.anchor; + return [selectionStart, selectionEnd]; + }; + var getLastSelectedAreaRange = function() { + var selectionStart = cm.getCursor(); + var selectionEnd = cm.getCursor(); + var block = lastSelection.visualBlock; + if (block) { + var width = block.width; + var height = block.height; + selectionEnd = Pos(selectionStart.line + height, selectionStart.ch + width); + var selections = []; + // selectBlock creates a 'proper' rectangular block. + // We do not want that in all cases, so we manually set selections. + for (var i = selectionStart.line; i < selectionEnd.line; i++) { + var anchor = Pos(i, selectionStart.ch); + var head = Pos(i, selectionEnd.ch); + var range = {anchor: anchor, head: head}; + selections.push(range); + } + cm.setSelections(selections); + } else { + var start = lastSelection.anchorMark.find(); + var end = lastSelection.headMark.find(); + var line = end.line - start.line; + var ch = end.ch - start.ch; + selectionEnd = {line: selectionEnd.line + line, ch: line ? selectionEnd.ch : ch + selectionEnd.ch}; + if (lastSelection.visualLine) { + selectionStart = Pos(selectionStart.line, 0); + selectionEnd = Pos(selectionEnd.line, lineLength(cm, selectionEnd.line)); + } + cm.setSelection(selectionStart, selectionEnd); + } + return [selectionStart, selectionEnd]; + }; + if (!vim.visualMode) { + // In case of replaying the action. + return getLastSelectedAreaRange(); + } else { + return getCurrentSelectedAreaRange(); + } + } + // Updates the previous selection with the current selection's values. This + // should only be called in visual mode. + function updateLastSelection(cm, vim) { + var anchor = vim.sel.anchor; + var head = vim.sel.head; + // To accommodate the effect of lastPastedText in the last selection + if (vim.lastPastedText) { + head = cm.posFromIndex(cm.indexFromPos(anchor) + vim.lastPastedText.length); + vim.lastPastedText = null; + } + vim.lastSelection = {'anchorMark': cm.setBookmark(anchor), + 'headMark': cm.setBookmark(head), + 'anchor': copyCursor(anchor), + 'head': copyCursor(head), + 'visualMode': vim.visualMode, + 'visualLine': vim.visualLine, + 'visualBlock': vim.visualBlock}; + } + function expandSelection(cm, start, end) { + var sel = cm.state.vim.sel; + var head = sel.head; + var anchor = sel.anchor; + var tmp; + if (cursorIsBefore(end, start)) { + tmp = end; + end = start; + start = tmp; + } + if (cursorIsBefore(head, anchor)) { + head = cursorMin(start, head); + anchor = cursorMax(anchor, end); + } else { + anchor = cursorMin(start, anchor); + head = cursorMax(head, end); + head = offsetCursor(head, 0, -1); + if (head.ch == -1 && head.line != cm.firstLine()) { + head = Pos(head.line - 1, lineLength(cm, head.line - 1)); + } + } + return [anchor, head]; + } + /** + * Updates the CodeMirror selection to match the provided vim selection. + * If no arguments are given, it uses the current vim selection state. + */ + function updateCmSelection(cm, sel, mode) { + var vim = cm.state.vim; + sel = sel || vim.sel; + var mode = mode || + vim.visualLine ? 'line' : vim.visualBlock ? 'block' : 'char'; + var cmSel = makeCmSelection(cm, sel, mode); + cm.setSelections(cmSel.ranges, cmSel.primary); + updateFakeCursor(cm); + } + function makeCmSelection(cm, sel, mode, exclusive) { + var head = copyCursor(sel.head); + var anchor = copyCursor(sel.anchor); + if (mode == 'char') { + var headOffset = !exclusive && !cursorIsBefore(sel.head, sel.anchor) ? 1 : 0; + var anchorOffset = cursorIsBefore(sel.head, sel.anchor) ? 1 : 0; + head = offsetCursor(sel.head, 0, headOffset); + anchor = offsetCursor(sel.anchor, 0, anchorOffset); + return { + ranges: [{anchor: anchor, head: head}], + primary: 0 + }; + } else if (mode == 'line') { + if (!cursorIsBefore(sel.head, sel.anchor)) { + anchor.ch = 0; + + var lastLine = cm.lastLine(); + if (head.line > lastLine) { + head.line = lastLine; + } + head.ch = lineLength(cm, head.line); + } else { + head.ch = 0; + anchor.ch = lineLength(cm, anchor.line); + } + return { + ranges: [{anchor: anchor, head: head}], + primary: 0 + }; + } else if (mode == 'block') { + var top = Math.min(anchor.line, head.line), + left = Math.min(anchor.ch, head.ch), + bottom = Math.max(anchor.line, head.line), + right = Math.max(anchor.ch, head.ch) + 1; + var height = bottom - top + 1; + var primary = head.line == top ? 0 : height - 1; + var ranges = []; + for (var i = 0; i < height; i++) { + ranges.push({ + anchor: Pos(top + i, left), + head: Pos(top + i, right) + }); + } + return { + ranges: ranges, + primary: primary + }; + } + } + function getHead(cm) { + var cur = cm.getCursor('head'); + if (cm.getSelection().length == 1) { + // Small corner case when only 1 character is selected. The "real" + // head is the left of head and anchor. + cur = cursorMin(cur, cm.getCursor('anchor')); + } + return cur; + } + + /** + * If moveHead is set to false, the CodeMirror selection will not be + * touched. The caller assumes the responsibility of putting the cursor + * in the right place. + */ + function exitVisualMode(cm, moveHead) { + var vim = cm.state.vim; + if (moveHead !== false) { + cm.setCursor(clipCursorToContent(cm, vim.sel.head)); + } + updateLastSelection(cm, vim); + vim.visualMode = false; + vim.visualLine = false; + vim.visualBlock = false; + CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); + if (vim.fakeCursor) { + vim.fakeCursor.clear(); + } + } + + // Remove any trailing newlines from the selection. For + // example, with the caret at the start of the last word on the line, + // 'dw' should word, but not the newline, while 'w' should advance the + // caret to the first character of the next line. + function clipToLine(cm, curStart, curEnd) { + var selection = cm.getRange(curStart, curEnd); + // Only clip if the selection ends with trailing newline + whitespace + if (/\n\s*$/.test(selection)) { + var lines = selection.split('\n'); + // We know this is all whitespace. + lines.pop(); + + // Cases: + // 1. Last word is an empty line - do not clip the trailing '\n' + // 2. Last word is not an empty line - clip the trailing '\n' + var line; + // Find the line containing the last word, and clip all whitespace up + // to it. + for (var line = lines.pop(); lines.length > 0 && line && isWhiteSpaceString(line); line = lines.pop()) { + curEnd.line--; + curEnd.ch = 0; + } + // If the last word is not an empty line, clip an additional newline + if (line) { + curEnd.line--; + curEnd.ch = lineLength(cm, curEnd.line); + } else { + curEnd.ch = 0; + } + } + } + + // Expand the selection to line ends. + function expandSelectionToLine(_cm, curStart, curEnd) { + curStart.ch = 0; + curEnd.ch = 0; + curEnd.line++; + } + + function findFirstNonWhiteSpaceCharacter(text) { + if (!text) { + return 0; + } + var firstNonWS = text.search(/\S/); + return firstNonWS == -1 ? text.length : firstNonWS; + } + + function expandWordUnderCursor(cm, inclusive, _forward, bigWord, noSymbol) { + var cur = getHead(cm); + var line = cm.getLine(cur.line); + var idx = cur.ch; + + // Seek to first word or non-whitespace character, depending on if + // noSymbol is true. + var test = noSymbol ? wordCharTest[0] : bigWordCharTest [0]; + while (!test(line.charAt(idx))) { + idx++; + if (idx >= line.length) { return null; } + } + + if (bigWord) { + test = bigWordCharTest[0]; + } else { + test = wordCharTest[0]; + if (!test(line.charAt(idx))) { + test = wordCharTest[1]; + } + } + + var end = idx, start = idx; + while (test(line.charAt(end)) && end < line.length) { end++; } + while (test(line.charAt(start)) && start >= 0) { start--; } + start++; + + if (inclusive) { + // If present, include all whitespace after word. + // Otherwise, include all whitespace before word, except indentation. + var wordEnd = end; + while (/\s/.test(line.charAt(end)) && end < line.length) { end++; } + if (wordEnd == end) { + var wordStart = start; + while (/\s/.test(line.charAt(start - 1)) && start > 0) { start--; } + if (!start) { start = wordStart; } + } + } + return { start: Pos(cur.line, start), end: Pos(cur.line, end) }; + } + + function recordJumpPosition(cm, oldCur, newCur) { + if (!cursorEqual(oldCur, newCur)) { + vimGlobalState.jumpList.add(cm, oldCur, newCur); + } + } + + function recordLastCharacterSearch(increment, args) { + vimGlobalState.lastCharacterSearch.increment = increment; + vimGlobalState.lastCharacterSearch.forward = args.forward; + vimGlobalState.lastCharacterSearch.selectedCharacter = args.selectedCharacter; + } + + var symbolToMode = { + '(': 'bracket', ')': 'bracket', '{': 'bracket', '}': 'bracket', + '[': 'section', ']': 'section', + '*': 'comment', '/': 'comment', + 'm': 'method', 'M': 'method', + '#': 'preprocess' + }; + var findSymbolModes = { + bracket: { + isComplete: function(state) { + if (state.nextCh === state.symb) { + state.depth++; + if (state.depth >= 1)return true; + } else if (state.nextCh === state.reverseSymb) { + state.depth--; + } + return false; + } + }, + section: { + init: function(state) { + state.curMoveThrough = true; + state.symb = (state.forward ? ']' : '[') === state.symb ? '{' : '}'; + }, + isComplete: function(state) { + return state.index === 0 && state.nextCh === state.symb; + } + }, + comment: { + isComplete: function(state) { + var found = state.lastCh === '*' && state.nextCh === '/'; + state.lastCh = state.nextCh; + return found; + } + }, + // TODO: The original Vim implementation only operates on level 1 and 2. + // The current implementation doesn't check for code block level and + // therefore it operates on any levels. + method: { + init: function(state) { + state.symb = (state.symb === 'm' ? '{' : '}'); + state.reverseSymb = state.symb === '{' ? '}' : '{'; + }, + isComplete: function(state) { + if (state.nextCh === state.symb)return true; + return false; + } + }, + preprocess: { + init: function(state) { + state.index = 0; + }, + isComplete: function(state) { + if (state.nextCh === '#') { + var token = state.lineText.match(/#(\w+)/)[1]; + if (token === 'endif') { + if (state.forward && state.depth === 0) { + return true; + } + state.depth++; + } else if (token === 'if') { + if (!state.forward && state.depth === 0) { + return true; + } + state.depth--; + } + if (token === 'else' && state.depth === 0)return true; + } + return false; + } + } + }; + function findSymbol(cm, repeat, forward, symb) { + var cur = copyCursor(cm.getCursor()); + var increment = forward ? 1 : -1; + var endLine = forward ? cm.lineCount() : -1; + var curCh = cur.ch; + var line = cur.line; + var lineText = cm.getLine(line); + var state = { + lineText: lineText, + nextCh: lineText.charAt(curCh), + lastCh: null, + index: curCh, + symb: symb, + reverseSymb: (forward ? { ')': '(', '}': '{' } : { '(': ')', '{': '}' })[symb], + forward: forward, + depth: 0, + curMoveThrough: false + }; + var mode = symbolToMode[symb]; + if (!mode)return cur; + var init = findSymbolModes[mode].init; + var isComplete = findSymbolModes[mode].isComplete; + if (init) { init(state); } + while (line !== endLine && repeat) { + state.index += increment; + state.nextCh = state.lineText.charAt(state.index); + if (!state.nextCh) { + line += increment; + state.lineText = cm.getLine(line) || ''; + if (increment > 0) { + state.index = 0; + } else { + var lineLen = state.lineText.length; + state.index = (lineLen > 0) ? (lineLen-1) : 0; + } + state.nextCh = state.lineText.charAt(state.index); + } + if (isComplete(state)) { + cur.line = line; + cur.ch = state.index; + repeat--; + } + } + if (state.nextCh || state.curMoveThrough) { + return Pos(line, state.index); + } + return cur; + } + + /** + * Returns the boundaries of the next word. If the cursor in the middle of + * the word, then returns the boundaries of the current word, starting at + * the cursor. If the cursor is at the start/end of a word, and we are going + * forward/backward, respectively, find the boundaries of the next word. + * + * @param {CodeMirror} cm CodeMirror object. + * @param {Cursor} cur The cursor position. + * @param {boolean} forward True to search forward. False to search + * backward. + * @param {boolean} bigWord True if punctuation count as part of the word. + * False if only [a-zA-Z0-9] characters count as part of the word. + * @param {boolean} emptyLineIsWord True if empty lines should be treated + * as words. + * @return {Object{from:number, to:number, line: number}} The boundaries of + * the word, or null if there are no more words. + */ + function findWord(cm, cur, forward, bigWord, emptyLineIsWord) { + var lineNum = cur.line; + var pos = cur.ch; + var line = cm.getLine(lineNum); + var dir = forward ? 1 : -1; + var charTests = bigWord ? bigWordCharTest: wordCharTest; + + if (emptyLineIsWord && line == '') { + lineNum += dir; + line = cm.getLine(lineNum); + if (!isLine(cm, lineNum)) { + return null; + } + pos = (forward) ? 0 : line.length; + } + + while (true) { + if (emptyLineIsWord && line == '') { + return { from: 0, to: 0, line: lineNum }; + } + var stop = (dir > 0) ? line.length : -1; + var wordStart = stop, wordEnd = stop; + // Find bounds of next word. + while (pos != stop) { + var foundWord = false; + for (var i = 0; i < charTests.length && !foundWord; ++i) { + if (charTests[i](line.charAt(pos))) { + wordStart = pos; + // Advance to end of word. + while (pos != stop && charTests[i](line.charAt(pos))) { + pos += dir; + } + wordEnd = pos; + foundWord = wordStart != wordEnd; + if (wordStart == cur.ch && lineNum == cur.line && + wordEnd == wordStart + dir) { + // We started at the end of a word. Find the next one. + continue; + } else { + return { + from: Math.min(wordStart, wordEnd + 1), + to: Math.max(wordStart, wordEnd), + line: lineNum }; + } + } + } + if (!foundWord) { + pos += dir; + } + } + // Advance to next/prev line. + lineNum += dir; + if (!isLine(cm, lineNum)) { + return null; + } + line = cm.getLine(lineNum); + pos = (dir > 0) ? 0 : line.length; + } + } + + /** + * @param {CodeMirror} cm CodeMirror object. + * @param {Pos} cur The position to start from. + * @param {int} repeat Number of words to move past. + * @param {boolean} forward True to search forward. False to search + * backward. + * @param {boolean} wordEnd True to move to end of word. False to move to + * beginning of word. + * @param {boolean} bigWord True if punctuation count as part of the word. + * False if only alphabet characters count as part of the word. + * @return {Cursor} The position the cursor should move to. + */ + function moveToWord(cm, cur, repeat, forward, wordEnd, bigWord) { + var curStart = copyCursor(cur); + var words = []; + if (forward && !wordEnd || !forward && wordEnd) { + repeat++; + } + // For 'e', empty lines are not considered words, go figure. + var emptyLineIsWord = !(forward && wordEnd); + for (var i = 0; i < repeat; i++) { + var word = findWord(cm, cur, forward, bigWord, emptyLineIsWord); + if (!word) { + var eodCh = lineLength(cm, cm.lastLine()); + words.push(forward + ? {line: cm.lastLine(), from: eodCh, to: eodCh} + : {line: 0, from: 0, to: 0}); + break; + } + words.push(word); + cur = Pos(word.line, forward ? (word.to - 1) : word.from); + } + var shortCircuit = words.length != repeat; + var firstWord = words[0]; + var lastWord = words.pop(); + if (forward && !wordEnd) { + // w + if (!shortCircuit && (firstWord.from != curStart.ch || firstWord.line != curStart.line)) { + // We did not start in the middle of a word. Discard the extra word at the end. + lastWord = words.pop(); + } + return Pos(lastWord.line, lastWord.from); + } else if (forward && wordEnd) { + return Pos(lastWord.line, lastWord.to - 1); + } else if (!forward && wordEnd) { + // ge + if (!shortCircuit && (firstWord.to != curStart.ch || firstWord.line != curStart.line)) { + // We did not start in the middle of a word. Discard the extra word at the end. + lastWord = words.pop(); + } + return Pos(lastWord.line, lastWord.to); + } else { + // b + return Pos(lastWord.line, lastWord.from); + } + } + + function moveToCharacter(cm, repeat, forward, character) { + var cur = cm.getCursor(); + var start = cur.ch; + var idx; + for (var i = 0; i < repeat; i ++) { + var line = cm.getLine(cur.line); + idx = charIdxInLine(start, line, character, forward, true); + if (idx == -1) { + return null; + } + start = idx; + } + return Pos(cm.getCursor().line, idx); + } + + function moveToColumn(cm, repeat) { + // repeat is always >= 1, so repeat - 1 always corresponds + // to the column we want to go to. + var line = cm.getCursor().line; + return clipCursorToContent(cm, Pos(line, repeat - 1)); + } + + function updateMark(cm, vim, markName, pos) { + if (!inArray(markName, validMarks)) { + return; + } + if (vim.marks[markName]) { + vim.marks[markName].clear(); + } + vim.marks[markName] = cm.setBookmark(pos); + } + + function charIdxInLine(start, line, character, forward, includeChar) { + // Search for char in line. + // motion_options: {forward, includeChar} + // If includeChar = true, include it too. + // If forward = true, search forward, else search backwards. + // If char is not found on this line, do nothing + var idx; + if (forward) { + idx = line.indexOf(character, start + 1); + if (idx != -1 && !includeChar) { + idx -= 1; + } + } else { + idx = line.lastIndexOf(character, start - 1); + if (idx != -1 && !includeChar) { + idx += 1; + } + } + return idx; + } + + function findParagraph(cm, head, repeat, dir, inclusive) { + var line = head.line; + var min = cm.firstLine(); + var max = cm.lastLine(); + var start, end, i = line; + function isEmpty(i) { return !cm.getLine(i); } + function isBoundary(i, dir, any) { + if (any) { return isEmpty(i) != isEmpty(i + dir); } + return !isEmpty(i) && isEmpty(i + dir); + } + if (dir) { + while (min <= i && i <= max && repeat > 0) { + if (isBoundary(i, dir)) { repeat--; } + i += dir; + } + return new Pos(i, 0); + } + + var vim = cm.state.vim; + if (vim.visualLine && isBoundary(line, 1, true)) { + var anchor = vim.sel.anchor; + if (isBoundary(anchor.line, -1, true)) { + if (!inclusive || anchor.line != line) { + line += 1; + } + } + } + var startState = isEmpty(line); + for (i = line; i <= max && repeat; i++) { + if (isBoundary(i, 1, true)) { + if (!inclusive || isEmpty(i) != startState) { + repeat--; + } + } + } + end = new Pos(i, 0); + // select boundary before paragraph for the last one + if (i > max && !startState) { startState = true; } + else { inclusive = false; } + for (i = line; i > min; i--) { + if (!inclusive || isEmpty(i) == startState || i == line) { + if (isBoundary(i, -1, true)) { break; } + } + } + start = new Pos(i, 0); + return { start: start, end: end }; + } + + // TODO: perhaps this finagling of start and end positions belonds + // in codemirror/replaceRange? + function selectCompanionObject(cm, head, symb, inclusive) { + var cur = head, start, end; + + var bracketRegexp = ({ + '(': /[()]/, ')': /[()]/, + '[': /[[\]]/, ']': /[[\]]/, + '{': /[{}]/, '}': /[{}]/})[symb]; + var openSym = ({ + '(': '(', ')': '(', + '[': '[', ']': '[', + '{': '{', '}': '{'})[symb]; + var curChar = cm.getLine(cur.line).charAt(cur.ch); + // Due to the behavior of scanForBracket, we need to add an offset if the + // cursor is on a matching open bracket. + var offset = curChar === openSym ? 1 : 0; + + start = cm.scanForBracket(Pos(cur.line, cur.ch + offset), -1, null, {'bracketRegex': bracketRegexp}); + end = cm.scanForBracket(Pos(cur.line, cur.ch + offset), 1, null, {'bracketRegex': bracketRegexp}); + + if (!start || !end) { + return { start: cur, end: cur }; + } + + start = start.pos; + end = end.pos; + + if ((start.line == end.line && start.ch > end.ch) + || (start.line > end.line)) { + var tmp = start; + start = end; + end = tmp; + } + + if (inclusive) { + end.ch += 1; + } else { + start.ch += 1; + } + + return { start: start, end: end }; + } + + // Takes in a symbol and a cursor and tries to simulate text objects that + // have identical opening and closing symbols + // TODO support across multiple lines + function findBeginningAndEnd(cm, head, symb, inclusive) { + var cur = copyCursor(head); + var line = cm.getLine(cur.line); + var chars = line.split(''); + var start, end, i, len; + var firstIndex = chars.indexOf(symb); + + // the decision tree is to always look backwards for the beginning first, + // but if the cursor is in front of the first instance of the symb, + // then move the cursor forward + if (cur.ch < firstIndex) { + cur.ch = firstIndex; + // Why is this line even here??? + // cm.setCursor(cur.line, firstIndex+1); + } + // otherwise if the cursor is currently on the closing symbol + else if (firstIndex < cur.ch && chars[cur.ch] == symb) { + end = cur.ch; // assign end to the current cursor + --cur.ch; // make sure to look backwards + } + + // if we're currently on the symbol, we've got a start + if (chars[cur.ch] == symb && !end) { + start = cur.ch + 1; // assign start to ahead of the cursor + } else { + // go backwards to find the start + for (i = cur.ch; i > -1 && !start; i--) { + if (chars[i] == symb) { + start = i + 1; + } + } + } + + // look forwards for the end symbol + if (start && !end) { + for (i = start, len = chars.length; i < len && !end; i++) { + if (chars[i] == symb) { + end = i; + } + } + } + + // nothing found + if (!start || !end) { + return { start: cur, end: cur }; + } + + // include the symbols + if (inclusive) { + --start; ++end; + } + + return { + start: Pos(cur.line, start), + end: Pos(cur.line, end) + }; + } + + // Search functions + defineOption('pcre', true, 'boolean'); + function SearchState() {} + SearchState.prototype = { + getQuery: function() { + return vimGlobalState.query; + }, + setQuery: function(query) { + vimGlobalState.query = query; + }, + getOverlay: function() { + return this.searchOverlay; + }, + setOverlay: function(overlay) { + this.searchOverlay = overlay; + }, + isReversed: function() { + return vimGlobalState.isReversed; + }, + setReversed: function(reversed) { + vimGlobalState.isReversed = reversed; + }, + getScrollbarAnnotate: function() { + return this.annotate; + }, + setScrollbarAnnotate: function(annotate) { + this.annotate = annotate; + } + }; + function getSearchState(cm) { + var vim = cm.state.vim; + return vim.searchState_ || (vim.searchState_ = new SearchState()); + } + function dialog(cm, template, shortText, onClose, options) { + if (cm.openDialog) { + cm.openDialog(template, onClose, { bottom: true, value: options.value, + onKeyDown: options.onKeyDown, onKeyUp: options.onKeyUp, + selectValueOnOpen: false}); + } + else { + onClose(prompt(shortText, '')); + } + } + function splitBySlash(argString) { + var slashes = findUnescapedSlashes(argString) || []; + if (!slashes.length) return []; + var tokens = []; + // in case of strings like foo/bar + if (slashes[0] !== 0) return; + for (var i = 0; i < slashes.length; i++) { + if (typeof slashes[i] == 'number') + tokens.push(argString.substring(slashes[i] + 1, slashes[i+1])); + } + return tokens; + } + + function findUnescapedSlashes(str) { + var escapeNextChar = false; + var slashes = []; + for (var i = 0; i < str.length; i++) { + var c = str.charAt(i); + if (!escapeNextChar && c == '/') { + slashes.push(i); + } + escapeNextChar = !escapeNextChar && (c == '\\'); + } + return slashes; + } + + // Translates a search string from ex (vim) syntax into javascript form. + function translateRegex(str) { + // When these match, add a '\' if unescaped or remove one if escaped. + var specials = '|(){'; + // Remove, but never add, a '\' for these. + var unescape = '}'; + var escapeNextChar = false; + var out = []; + for (var i = -1; i < str.length; i++) { + var c = str.charAt(i) || ''; + var n = str.charAt(i+1) || ''; + var specialComesNext = (n && specials.indexOf(n) != -1); + if (escapeNextChar) { + if (c !== '\\' || !specialComesNext) { + out.push(c); + } + escapeNextChar = false; + } else { + if (c === '\\') { + escapeNextChar = true; + // Treat the unescape list as special for removing, but not adding '\'. + if (n && unescape.indexOf(n) != -1) { + specialComesNext = true; + } + // Not passing this test means removing a '\'. + if (!specialComesNext || n === '\\') { + out.push(c); + } + } else { + out.push(c); + if (specialComesNext && n !== '\\') { + out.push('\\'); + } + } + } + } + return out.join(''); + } + + // Translates the replace part of a search and replace from ex (vim) syntax into + // javascript form. Similar to translateRegex, but additionally fixes back references + // (translates '\[0..9]' to '$[0..9]') and follows different rules for escaping '$'. + var charUnescapes = {'\\n': '\n', '\\r': '\r', '\\t': '\t'}; + function translateRegexReplace(str) { + var escapeNextChar = false; + var out = []; + for (var i = -1; i < str.length; i++) { + var c = str.charAt(i) || ''; + var n = str.charAt(i+1) || ''; + if (charUnescapes[c + n]) { + out.push(charUnescapes[c+n]); + i++; + } else if (escapeNextChar) { + // At any point in the loop, escapeNextChar is true if the previous + // character was a '\' and was not escaped. + out.push(c); + escapeNextChar = false; + } else { + if (c === '\\') { + escapeNextChar = true; + if ((isNumber(n) || n === '$')) { + out.push('$'); + } else if (n !== '/' && n !== '\\') { + out.push('\\'); + } + } else { + if (c === '$') { + out.push('$'); + } + out.push(c); + if (n === '/') { + out.push('\\'); + } + } + } + } + return out.join(''); + } + + // Unescape \ and / in the replace part, for PCRE mode. + var unescapes = {'\\/': '/', '\\\\': '\\', '\\n': '\n', '\\r': '\r', '\\t': '\t'}; + function unescapeRegexReplace(str) { + var stream = new CodeMirror.StringStream(str); + var output = []; + while (!stream.eol()) { + // Search for \. + while (stream.peek() && stream.peek() != '\\') { + output.push(stream.next()); + } + var matched = false; + for (var matcher in unescapes) { + if (stream.match(matcher, true)) { + matched = true; + output.push(unescapes[matcher]); + break; + } + } + if (!matched) { + // Don't change anything + output.push(stream.next()); + } + } + return output.join(''); + } + + /** + * Extract the regular expression from the query and return a Regexp object. + * Returns null if the query is blank. + * If ignoreCase is passed in, the Regexp object will have the 'i' flag set. + * If smartCase is passed in, and the query contains upper case letters, + * then ignoreCase is overridden, and the 'i' flag will not be set. + * If the query contains the /i in the flag part of the regular expression, + * then both ignoreCase and smartCase are ignored, and 'i' will be passed + * through to the Regex object. + */ + function parseQuery(query, ignoreCase, smartCase) { + // First update the last search register + var lastSearchRegister = vimGlobalState.registerController.getRegister('/'); + lastSearchRegister.setText(query); + // Check if the query is already a regex. + if (query instanceof RegExp) { return query; } + // First try to extract regex + flags from the input. If no flags found, + // extract just the regex. IE does not accept flags directly defined in + // the regex string in the form /regex/flags + var slashes = findUnescapedSlashes(query); + var regexPart; + var forceIgnoreCase; + if (!slashes.length) { + // Query looks like 'regexp' + regexPart = query; + } else { + // Query looks like 'regexp/...' + regexPart = query.substring(0, slashes[0]); + var flagsPart = query.substring(slashes[0]); + forceIgnoreCase = (flagsPart.indexOf('i') != -1); + } + if (!regexPart) { + return null; + } + if (!getOption('pcre')) { + regexPart = translateRegex(regexPart); + } + if (smartCase) { + ignoreCase = (/^[^A-Z]*$/).test(regexPart); + } + var regexp = new RegExp(regexPart, + (ignoreCase || forceIgnoreCase) ? 'i' : undefined); + return regexp; + } + function showConfirm(cm, text) { + if (cm.openNotification) { + cm.openNotification('' + text + '', + {bottom: true, duration: 5000}); + } else { + alert(text); + } + } + function makePrompt(prefix, desc) { + var raw = '' + + (prefix || "") + ''; + if (desc) + raw += ' ' + desc + ''; + return raw; + } + var searchPromptDesc = '(Javascript regexp)'; + function showPrompt(cm, options) { + var shortText = (options.prefix || '') + ' ' + (options.desc || ''); + var prompt = makePrompt(options.prefix, options.desc); + dialog(cm, prompt, shortText, options.onClose, options); + } + function regexEqual(r1, r2) { + if (r1 instanceof RegExp && r2 instanceof RegExp) { + var props = ['global', 'multiline', 'ignoreCase', 'source']; + for (var i = 0; i < props.length; i++) { + var prop = props[i]; + if (r1[prop] !== r2[prop]) { + return false; + } + } + return true; + } + return false; + } + // Returns true if the query is valid. + function updateSearchQuery(cm, rawQuery, ignoreCase, smartCase) { + if (!rawQuery) { + return; + } + var state = getSearchState(cm); + var query = parseQuery(rawQuery, !!ignoreCase, !!smartCase); + if (!query) { + return; + } + highlightSearchMatches(cm, query); + if (regexEqual(query, state.getQuery())) { + return query; + } + state.setQuery(query); + return query; + } + function searchOverlay(query) { + if (query.source.charAt(0) == '^') { + var matchSol = true; + } + return { + token: function(stream) { + if (matchSol && !stream.sol()) { + stream.skipToEnd(); + return; + } + var match = stream.match(query, false); + if (match) { + if (match[0].length == 0) { + // Matched empty string, skip to next. + stream.next(); + return 'searching'; + } + if (!stream.sol()) { + // Backtrack 1 to match \b + stream.backUp(1); + if (!query.exec(stream.next() + match[0])) { + stream.next(); + return null; + } + } + stream.match(query); + return 'searching'; + } + while (!stream.eol()) { + stream.next(); + if (stream.match(query, false)) break; + } + }, + query: query + }; + } + function highlightSearchMatches(cm, query) { + var searchState = getSearchState(cm); + var overlay = searchState.getOverlay(); + if (!overlay || query != overlay.query) { + if (overlay) { + cm.removeOverlay(overlay); + } + overlay = searchOverlay(query); + cm.addOverlay(overlay); + if (cm.showMatchesOnScrollbar) { + if (searchState.getScrollbarAnnotate()) { + searchState.getScrollbarAnnotate().clear(); + } + searchState.setScrollbarAnnotate(cm.showMatchesOnScrollbar(query)); + } + searchState.setOverlay(overlay); + } + } + function findNext(cm, prev, query, repeat) { + if (repeat === undefined) { repeat = 1; } + return cm.operation(function() { + var pos = cm.getCursor(); + var cursor = cm.getSearchCursor(query, pos); + for (var i = 0; i < repeat; i++) { + var found = cursor.find(prev); + if (i == 0 && found && cursorEqual(cursor.from(), pos)) { found = cursor.find(prev); } + if (!found) { + // SearchCursor may have returned null because it hit EOF, wrap + // around and try again. + cursor = cm.getSearchCursor(query, + (prev) ? Pos(cm.lastLine()) : Pos(cm.firstLine(), 0) ); + if (!cursor.find(prev)) { + return; + } + } + } + return cursor.from(); + }); + } + function clearSearchHighlight(cm) { + var state = getSearchState(cm); + cm.removeOverlay(getSearchState(cm).getOverlay()); + state.setOverlay(null); + if (state.getScrollbarAnnotate()) { + state.getScrollbarAnnotate().clear(); + state.setScrollbarAnnotate(null); + } + } + /** + * Check if pos is in the specified range, INCLUSIVE. + * Range can be specified with 1 or 2 arguments. + * If the first range argument is an array, treat it as an array of line + * numbers. Match pos against any of the lines. + * If the first range argument is a number, + * if there is only 1 range argument, check if pos has the same line + * number + * if there are 2 range arguments, then check if pos is in between the two + * range arguments. + */ + function isInRange(pos, start, end) { + if (typeof pos != 'number') { + // Assume it is a cursor position. Get the line number. + pos = pos.line; + } + if (start instanceof Array) { + return inArray(pos, start); + } else { + if (end) { + return (pos >= start && pos <= end); + } else { + return pos == start; + } + } + } + function getUserVisibleLines(cm) { + var scrollInfo = cm.getScrollInfo(); + var occludeToleranceTop = 6; + var occludeToleranceBottom = 10; + var from = cm.coordsChar({left:0, top: occludeToleranceTop + scrollInfo.top}, 'local'); + var bottomY = scrollInfo.clientHeight - occludeToleranceBottom + scrollInfo.top; + var to = cm.coordsChar({left:0, top: bottomY}, 'local'); + return {top: from.line, bottom: to.line}; + } + + var ExCommandDispatcher = function() { + this.buildCommandMap_(); + }; + ExCommandDispatcher.prototype = { + processCommand: function(cm, input, opt_params) { + var that = this; + cm.operation(function () { + cm.curOp.isVimOp = true; + that._processCommand(cm, input, opt_params); + }); + }, + _processCommand: function(cm, input, opt_params) { + var vim = cm.state.vim; + var commandHistoryRegister = vimGlobalState.registerController.getRegister(':'); + var previousCommand = commandHistoryRegister.toString(); + if (vim.visualMode) { + exitVisualMode(cm); + } + var inputStream = new CodeMirror.StringStream(input); + // update ": with the latest command whether valid or invalid + commandHistoryRegister.setText(input); + var params = opt_params || {}; + params.input = input; + try { + this.parseInput_(cm, inputStream, params); + } catch(e) { + showConfirm(cm, e); + throw e; + } + var command; + var commandName; + if (!params.commandName) { + // If only a line range is defined, move to the line. + if (params.line !== undefined) { + commandName = 'move'; + } + } else { + command = this.matchCommand_(params.commandName); + if (command) { + commandName = command.name; + if (command.excludeFromCommandHistory) { + commandHistoryRegister.setText(previousCommand); + } + this.parseCommandArgs_(inputStream, params, command); + if (command.type == 'exToKey') { + // Handle Ex to Key mapping. + for (var i = 0; i < command.toKeys.length; i++) { + CodeMirror.Vim.handleKey(cm, command.toKeys[i], 'mapping'); + } + return; + } else if (command.type == 'exToEx') { + // Handle Ex to Ex mapping. + this.processCommand(cm, command.toInput); + return; + } + } + } + if (!commandName) { + showConfirm(cm, 'Not an editor command ":' + input + '"'); + return; + } + try { + exCommands[commandName](cm, params); + // Possibly asynchronous commands (e.g. substitute, which might have a + // user confirmation), are responsible for calling the callback when + // done. All others have it taken care of for them here. + if ((!command || !command.possiblyAsync) && params.callback) { + params.callback(); + } + } catch(e) { + showConfirm(cm, e); + throw e; + } + }, + parseInput_: function(cm, inputStream, result) { + inputStream.eatWhile(':'); + // Parse range. + if (inputStream.eat('%')) { + result.line = cm.firstLine(); + result.lineEnd = cm.lastLine(); + } else { + result.line = this.parseLineSpec_(cm, inputStream); + if (result.line !== undefined && inputStream.eat(',')) { + result.lineEnd = this.parseLineSpec_(cm, inputStream); + } + } + + // Parse command name. + var commandMatch = inputStream.match(/^(\w+)/); + if (commandMatch) { + result.commandName = commandMatch[1]; + } else { + result.commandName = inputStream.match(/.*/)[0]; + } + + return result; + }, + parseLineSpec_: function(cm, inputStream) { + var numberMatch = inputStream.match(/^(\d+)/); + if (numberMatch) { + return parseInt(numberMatch[1], 10) - 1; + } + switch (inputStream.next()) { + case '.': + return cm.getCursor().line; + case '$': + return cm.lastLine(); + case '\'': + var mark = cm.state.vim.marks[inputStream.next()]; + if (mark && mark.find()) { + return mark.find().line; + } + throw new Error('Mark not set'); + default: + inputStream.backUp(1); + return undefined; + } + }, + parseCommandArgs_: function(inputStream, params, command) { + if (inputStream.eol()) { + return; + } + params.argString = inputStream.match(/.*/)[0]; + // Parse command-line arguments + var delim = command.argDelimiter || /\s+/; + var args = trim(params.argString).split(delim); + if (args.length && args[0]) { + params.args = args; + } + }, + matchCommand_: function(commandName) { + // Return the command in the command map that matches the shortest + // prefix of the passed in command name. The match is guaranteed to be + // unambiguous if the defaultExCommandMap's shortNames are set up + // correctly. (see @code{defaultExCommandMap}). + for (var i = commandName.length; i > 0; i--) { + var prefix = commandName.substring(0, i); + if (this.commandMap_[prefix]) { + var command = this.commandMap_[prefix]; + if (command.name.indexOf(commandName) === 0) { + return command; + } + } + } + return null; + }, + buildCommandMap_: function() { + this.commandMap_ = {}; + for (var i = 0; i < defaultExCommandMap.length; i++) { + var command = defaultExCommandMap[i]; + var key = command.shortName || command.name; + this.commandMap_[key] = command; + } + }, + map: function(lhs, rhs, ctx) { + if (lhs != ':' && lhs.charAt(0) == ':') { + if (ctx) { throw Error('Mode not supported for ex mappings'); } + var commandName = lhs.substring(1); + if (rhs != ':' && rhs.charAt(0) == ':') { + // Ex to Ex mapping + this.commandMap_[commandName] = { + name: commandName, + type: 'exToEx', + toInput: rhs.substring(1), + user: true + }; + } else { + // Ex to key mapping + this.commandMap_[commandName] = { + name: commandName, + type: 'exToKey', + toKeys: rhs, + user: true + }; + } + } else { + if (rhs != ':' && rhs.charAt(0) == ':') { + // Key to Ex mapping. + var mapping = { + keys: lhs, + type: 'keyToEx', + exArgs: { input: rhs.substring(1) }, + user: true}; + if (ctx) { mapping.context = ctx; } + defaultKeymap.unshift(mapping); + } else { + // Key to key mapping + var mapping = { + keys: lhs, + type: 'keyToKey', + toKeys: rhs, + user: true + }; + if (ctx) { mapping.context = ctx; } + defaultKeymap.unshift(mapping); + } + } + }, + unmap: function(lhs, ctx) { + if (lhs != ':' && lhs.charAt(0) == ':') { + // Ex to Ex or Ex to key mapping + if (ctx) { throw Error('Mode not supported for ex mappings'); } + var commandName = lhs.substring(1); + if (this.commandMap_[commandName] && this.commandMap_[commandName].user) { + delete this.commandMap_[commandName]; + return; + } + } else { + // Key to Ex or key to key mapping + var keys = lhs; + for (var i = 0; i < defaultKeymap.length; i++) { + if (keys == defaultKeymap[i].keys + && defaultKeymap[i].context === ctx + && defaultKeymap[i].user) { + defaultKeymap.splice(i, 1); + return; + } + } + } + throw Error('No such mapping.'); + } + }; + + var exCommands = { + colorscheme: function(cm, params) { + if (!params.args || params.args.length < 1) { + showConfirm(cm, cm.getOption('theme')); + return; + } + cm.setOption('theme', params.args[0]); + }, + map: function(cm, params, ctx) { + var mapArgs = params.args; + if (!mapArgs || mapArgs.length < 2) { + if (cm) { + showConfirm(cm, 'Invalid mapping: ' + params.input); + } + return; + } + exCommandDispatcher.map(mapArgs[0], mapArgs[1], ctx); + }, + imap: function(cm, params) { this.map(cm, params, 'insert'); }, + nmap: function(cm, params) { this.map(cm, params, 'normal'); }, + vmap: function(cm, params) { this.map(cm, params, 'visual'); }, + unmap: function(cm, params, ctx) { + var mapArgs = params.args; + if (!mapArgs || mapArgs.length < 1) { + if (cm) { + showConfirm(cm, 'No such mapping: ' + params.input); + } + return; + } + exCommandDispatcher.unmap(mapArgs[0], ctx); + }, + move: function(cm, params) { + commandDispatcher.processCommand(cm, cm.state.vim, { + type: 'motion', + motion: 'moveToLineOrEdgeOfDocument', + motionArgs: { forward: false, explicitRepeat: true, + linewise: true }, + repeatOverride: params.line+1}); + }, + set: function(cm, params) { + var setArgs = params.args; + // Options passed through to the setOption/getOption calls. May be passed in by the + // local/global versions of the set command + var setCfg = params.setCfg || {}; + if (!setArgs || setArgs.length < 1) { + if (cm) { + showConfirm(cm, 'Invalid mapping: ' + params.input); + } + return; + } + var expr = setArgs[0].split('='); + var optionName = expr[0]; + var value = expr[1]; + var forceGet = false; + + if (optionName.charAt(optionName.length - 1) == '?') { + // If post-fixed with ?, then the set is actually a get. + if (value) { throw Error('Trailing characters: ' + params.argString); } + optionName = optionName.substring(0, optionName.length - 1); + forceGet = true; + } + if (value === undefined && optionName.substring(0, 2) == 'no') { + // To set boolean options to false, the option name is prefixed with + // 'no'. + optionName = optionName.substring(2); + value = false; + } + + var optionIsBoolean = options[optionName] && options[optionName].type == 'boolean'; + if (optionIsBoolean && value == undefined) { + // Calling set with a boolean option sets it to true. + value = true; + } + // If no value is provided, then we assume this is a get. + if (!optionIsBoolean && value === undefined || forceGet) { + var oldValue = getOption(optionName, cm, setCfg); + if (oldValue === true || oldValue === false) { + showConfirm(cm, ' ' + (oldValue ? '' : 'no') + optionName); + } else { + showConfirm(cm, ' ' + optionName + '=' + oldValue); + } + } else { + setOption(optionName, value, cm, setCfg); + } + }, + setlocal: function (cm, params) { + // setCfg is passed through to setOption + params.setCfg = {scope: 'local'}; + this.set(cm, params); + }, + setglobal: function (cm, params) { + // setCfg is passed through to setOption + params.setCfg = {scope: 'global'}; + this.set(cm, params); + }, + registers: function(cm, params) { + var regArgs = params.args; + var registers = vimGlobalState.registerController.registers; + var regInfo = '----------Registers----------

'; + if (!regArgs) { + for (var registerName in registers) { + var text = registers[registerName].toString(); + if (text.length) { + regInfo += '"' + registerName + ' ' + text + '
'; + } + } + } else { + var registerName; + regArgs = regArgs.join(''); + for (var i = 0; i < regArgs.length; i++) { + registerName = regArgs.charAt(i); + if (!vimGlobalState.registerController.isValidRegister(registerName)) { + continue; + } + var register = registers[registerName] || new Register(); + regInfo += '"' + registerName + ' ' + register.toString() + '
'; + } + } + showConfirm(cm, regInfo); + }, + sort: function(cm, params) { + var reverse, ignoreCase, unique, number; + function parseArgs() { + if (params.argString) { + var args = new CodeMirror.StringStream(params.argString); + if (args.eat('!')) { reverse = true; } + if (args.eol()) { return; } + if (!args.eatSpace()) { return 'Invalid arguments'; } + var opts = args.match(/[a-z]+/); + if (opts) { + opts = opts[0]; + ignoreCase = opts.indexOf('i') != -1; + unique = opts.indexOf('u') != -1; + var decimal = opts.indexOf('d') != -1 && 1; + var hex = opts.indexOf('x') != -1 && 1; + var octal = opts.indexOf('o') != -1 && 1; + if (decimal + hex + octal > 1) { return 'Invalid arguments'; } + number = decimal && 'decimal' || hex && 'hex' || octal && 'octal'; + } + if (args.match(/\/.*\//)) { return 'patterns not supported'; } + } + } + var err = parseArgs(); + if (err) { + showConfirm(cm, err + ': ' + params.argString); + return; + } + var lineStart = params.line || cm.firstLine(); + var lineEnd = params.lineEnd || params.line || cm.lastLine(); + if (lineStart == lineEnd) { return; } + var curStart = Pos(lineStart, 0); + var curEnd = Pos(lineEnd, lineLength(cm, lineEnd)); + var text = cm.getRange(curStart, curEnd).split('\n'); + var numberRegex = (number == 'decimal') ? /(-?)([\d]+)/ : + (number == 'hex') ? /(-?)(?:0x)?([0-9a-f]+)/i : + (number == 'octal') ? /([0-7]+)/ : null; + var radix = (number == 'decimal') ? 10 : (number == 'hex') ? 16 : (number == 'octal') ? 8 : null; + var numPart = [], textPart = []; + if (number) { + for (var i = 0; i < text.length; i++) { + if (numberRegex.exec(text[i])) { + numPart.push(text[i]); + } else { + textPart.push(text[i]); + } + } + } else { + textPart = text; + } + function compareFn(a, b) { + if (reverse) { var tmp; tmp = a; a = b; b = tmp; } + if (ignoreCase) { a = a.toLowerCase(); b = b.toLowerCase(); } + var anum = number && numberRegex.exec(a); + var bnum = number && numberRegex.exec(b); + if (!anum) { return a < b ? -1 : 1; } + anum = parseInt((anum[1] + anum[2]).toLowerCase(), radix); + bnum = parseInt((bnum[1] + bnum[2]).toLowerCase(), radix); + return anum - bnum; + } + numPart.sort(compareFn); + textPart.sort(compareFn); + text = (!reverse) ? textPart.concat(numPart) : numPart.concat(textPart); + if (unique) { // Remove duplicate lines + var textOld = text; + var lastLine; + text = []; + for (var i = 0; i < textOld.length; i++) { + if (textOld[i] != lastLine) { + text.push(textOld[i]); + } + lastLine = textOld[i]; + } + } + cm.replaceRange(text.join('\n'), curStart, curEnd); + }, + global: function(cm, params) { + // a global command is of the form + // :[range]g/pattern/[cmd] + // argString holds the string /pattern/[cmd] + var argString = params.argString; + if (!argString) { + showConfirm(cm, 'Regular Expression missing from global'); + return; + } + // range is specified here + var lineStart = (params.line !== undefined) ? params.line : cm.firstLine(); + var lineEnd = params.lineEnd || params.line || cm.lastLine(); + // get the tokens from argString + var tokens = splitBySlash(argString); + var regexPart = argString, cmd; + if (tokens.length) { + regexPart = tokens[0]; + cmd = tokens.slice(1, tokens.length).join('/'); + } + if (regexPart) { + // If regex part is empty, then use the previous query. Otherwise + // use the regex part as the new query. + try { + updateSearchQuery(cm, regexPart, true /** ignoreCase */, + true /** smartCase */); + } catch (e) { + showConfirm(cm, 'Invalid regex: ' + regexPart); + return; + } + } + // now that we have the regexPart, search for regex matches in the + // specified range of lines + var query = getSearchState(cm).getQuery(); + var matchedLines = [], content = ''; + for (var i = lineStart; i <= lineEnd; i++) { + var matched = query.test(cm.getLine(i)); + if (matched) { + matchedLines.push(i+1); + content+= cm.getLine(i) + '
'; + } + } + // if there is no [cmd], just display the list of matched lines + if (!cmd) { + showConfirm(cm, content); + return; + } + var index = 0; + var nextCommand = function() { + if (index < matchedLines.length) { + var command = matchedLines[index] + cmd; + exCommandDispatcher.processCommand(cm, command, { + callback: nextCommand + }); + } + index++; + }; + nextCommand(); + }, + substitute: function(cm, params) { + if (!cm.getSearchCursor) { + throw new Error('Search feature not available. Requires searchcursor.js or ' + + 'any other getSearchCursor implementation.'); + } + var argString = params.argString; + var tokens = argString ? splitBySlash(argString) : []; + var regexPart, replacePart = '', trailing, flagsPart, count; + var confirm = false; // Whether to confirm each replace. + var global = false; // True to replace all instances on a line, false to replace only 1. + if (tokens.length) { + regexPart = tokens[0]; + replacePart = tokens[1]; + if (replacePart !== undefined) { + if (getOption('pcre')) { + replacePart = unescapeRegexReplace(replacePart); + } else { + replacePart = translateRegexReplace(replacePart); + } + vimGlobalState.lastSubstituteReplacePart = replacePart; + } + trailing = tokens[2] ? tokens[2].split(' ') : []; + } else { + // either the argString is empty or its of the form ' hello/world' + // actually splitBySlash returns a list of tokens + // only if the string starts with a '/' + if (argString && argString.length) { + showConfirm(cm, 'Substitutions should be of the form ' + + ':s/pattern/replace/'); + return; + } + } + // After the 3rd slash, we can have flags followed by a space followed + // by count. + if (trailing) { + flagsPart = trailing[0]; + count = parseInt(trailing[1]); + if (flagsPart) { + if (flagsPart.indexOf('c') != -1) { + confirm = true; + flagsPart.replace('c', ''); + } + if (flagsPart.indexOf('g') != -1) { + global = true; + flagsPart.replace('g', ''); + } + regexPart = regexPart + '/' + flagsPart; + } + } + if (regexPart) { + // If regex part is empty, then use the previous query. Otherwise use + // the regex part as the new query. + try { + updateSearchQuery(cm, regexPart, true /** ignoreCase */, + true /** smartCase */); + } catch (e) { + showConfirm(cm, 'Invalid regex: ' + regexPart); + return; + } + } + replacePart = replacePart || vimGlobalState.lastSubstituteReplacePart; + if (replacePart === undefined) { + showConfirm(cm, 'No previous substitute regular expression'); + return; + } + var state = getSearchState(cm); + var query = state.getQuery(); + var lineStart = (params.line !== undefined) ? params.line : cm.getCursor().line; + var lineEnd = params.lineEnd || lineStart; + if (lineStart == cm.firstLine() && lineEnd == cm.lastLine()) { + lineEnd = Infinity; + } + if (count) { + lineStart = lineEnd; + lineEnd = lineStart + count - 1; + } + var startPos = clipCursorToContent(cm, Pos(lineStart, 0)); + var cursor = cm.getSearchCursor(query, startPos); + doReplace(cm, confirm, global, lineStart, lineEnd, cursor, query, replacePart, params.callback); + }, + redo: CodeMirror.commands.redo, + undo: CodeMirror.commands.undo, + write: function(cm) { + if (CodeMirror.commands.save) { + // If a save command is defined, call it. + CodeMirror.commands.save(cm); + } else if (cm.save) { + // Saves to text area if no save command is defined and cm.save() is available. + cm.save(); + } + }, + nohlsearch: function(cm) { + clearSearchHighlight(cm); + }, + yank: function (cm) { + var cur = copyCursor(cm.getCursor()); + var line = cur.line; + var lineText = cm.getLine(line); + vimGlobalState.registerController.pushText( + '0', 'yank', lineText, true, true); + }, + delmarks: function(cm, params) { + if (!params.argString || !trim(params.argString)) { + showConfirm(cm, 'Argument required'); + return; + } + + var state = cm.state.vim; + var stream = new CodeMirror.StringStream(trim(params.argString)); + while (!stream.eol()) { + stream.eatSpace(); + + // Record the streams position at the beginning of the loop for use + // in error messages. + var count = stream.pos; + + if (!stream.match(/[a-zA-Z]/, false)) { + showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); + return; + } + + var sym = stream.next(); + // Check if this symbol is part of a range + if (stream.match('-', true)) { + // This symbol is part of a range. + + // The range must terminate at an alphabetic character. + if (!stream.match(/[a-zA-Z]/, false)) { + showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); + return; + } + + var startMark = sym; + var finishMark = stream.next(); + // The range must terminate at an alphabetic character which + // shares the same case as the start of the range. + if (isLowerCase(startMark) && isLowerCase(finishMark) || + isUpperCase(startMark) && isUpperCase(finishMark)) { + var start = startMark.charCodeAt(0); + var finish = finishMark.charCodeAt(0); + if (start >= finish) { + showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); + return; + } + + // Because marks are always ASCII values, and we have + // determined that they are the same case, we can use + // their char codes to iterate through the defined range. + for (var j = 0; j <= finish - start; j++) { + var mark = String.fromCharCode(start + j); + delete state.marks[mark]; + } + } else { + showConfirm(cm, 'Invalid argument: ' + startMark + '-'); + return; + } + } else { + // This symbol is a valid mark, and is not part of a range. + delete state.marks[sym]; + } + } + } + }; + + var exCommandDispatcher = new ExCommandDispatcher(); + + /** + * @param {CodeMirror} cm CodeMirror instance we are in. + * @param {boolean} confirm Whether to confirm each replace. + * @param {Cursor} lineStart Line to start replacing from. + * @param {Cursor} lineEnd Line to stop replacing at. + * @param {RegExp} query Query for performing matches with. + * @param {string} replaceWith Text to replace matches with. May contain $1, + * $2, etc for replacing captured groups using Javascript replace. + * @param {function()} callback A callback for when the replace is done. + */ + function doReplace(cm, confirm, global, lineStart, lineEnd, searchCursor, query, + replaceWith, callback) { + // Set up all the functions. + cm.state.vim.exMode = true; + var done = false; + var lastPos = searchCursor.from(); + function replaceAll() { + cm.operation(function() { + while (!done) { + replace(); + next(); + } + stop(); + }); + } + function replace() { + var text = cm.getRange(searchCursor.from(), searchCursor.to()); + var newText = text.replace(query, replaceWith); + searchCursor.replace(newText); + } + function next() { + // The below only loops to skip over multiple occurrences on the same + // line when 'global' is not true. + while(searchCursor.findNext() && + isInRange(searchCursor.from(), lineStart, lineEnd)) { + if (!global && lastPos && searchCursor.from().line == lastPos.line) { + continue; + } + cm.scrollIntoView(searchCursor.from(), 30); + cm.setSelection(searchCursor.from(), searchCursor.to()); + lastPos = searchCursor.from(); + done = false; + return; + } + done = true; + } + function stop(close) { + if (close) { close(); } + cm.focus(); + if (lastPos) { + cm.setCursor(lastPos); + var vim = cm.state.vim; + vim.exMode = false; + vim.lastHPos = vim.lastHSPos = lastPos.ch; + } + if (callback) { callback(); } + } + function onPromptKeyDown(e, _value, close) { + // Swallow all keys. + CodeMirror.e_stop(e); + var keyName = CodeMirror.keyName(e); + switch (keyName) { + case 'Y': + replace(); next(); break; + case 'N': + next(); break; + case 'A': + // replaceAll contains a call to close of its own. We don't want it + // to fire too early or multiple times. + var savedCallback = callback; + callback = undefined; + cm.operation(replaceAll); + callback = savedCallback; + break; + case 'L': + replace(); + // fall through and exit. + case 'Q': + case 'Esc': + case 'Ctrl-C': + case 'Ctrl-[': + stop(close); + break; + } + if (done) { stop(close); } + return true; + } + + // Actually do replace. + next(); + if (done) { + showConfirm(cm, 'No matches for ' + query.source); + return; + } + if (!confirm) { + replaceAll(); + if (callback) { callback(); }; + return; + } + showPrompt(cm, { + prefix: 'replace with ' + replaceWith + ' (y/n/a/q/l)', + onKeyDown: onPromptKeyDown + }); + } + + CodeMirror.keyMap.vim = { + attach: attachVimMap, + detach: detachVimMap, + call: cmKey + }; + + function exitInsertMode(cm) { + var vim = cm.state.vim; + var macroModeState = vimGlobalState.macroModeState; + var insertModeChangeRegister = vimGlobalState.registerController.getRegister('.'); + var isPlaying = macroModeState.isPlaying; + var lastChange = macroModeState.lastInsertModeChanges; + // In case of visual block, the insertModeChanges are not saved as a + // single word, so we convert them to a single word + // so as to update the ". register as expected in real vim. + var text = []; + if (!isPlaying) { + var selLength = lastChange.inVisualBlock ? vim.lastSelection.visualBlock.height : 1; + var changes = lastChange.changes; + var text = []; + var i = 0; + // In case of multiple selections in blockwise visual, + // the inserted text, for example: 'foo', is stored as + // 'f', 'f', InsertModeKey 'o', 'o', 'o', 'o'. (if you have a block with 2 lines). + // We push the contents of the changes array as per the following: + // 1. In case of InsertModeKey, just increment by 1. + // 2. In case of a character, jump by selLength (2 in the example). + while (i < changes.length) { + // This loop will convert 'ffoooo' to 'foo'. + text.push(changes[i]); + if (changes[i] instanceof InsertModeKey) { + i++; + } else { + i+= selLength; + } + } + lastChange.changes = text; + cm.off('change', onChange); + CodeMirror.off(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown); + } + if (!isPlaying && vim.insertModeRepeat > 1) { + // Perform insert mode repeat for commands like 3,a and 3,o. + repeatLastEdit(cm, vim, vim.insertModeRepeat - 1, + true /** repeatForInsert */); + vim.lastEditInputState.repeatOverride = vim.insertModeRepeat; + } + delete vim.insertModeRepeat; + vim.insertMode = false; + cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1); + cm.setOption('keyMap', 'vim'); + cm.setOption('disableInput', true); + cm.toggleOverwrite(false); // exit replace mode if we were in it. + // update the ". register before exiting insert mode + insertModeChangeRegister.setText(lastChange.changes.join('')); + CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); + if (macroModeState.isRecording) { + logInsertModeChange(macroModeState); + } + } + + function _mapCommand(command) { + defaultKeymap.unshift(command); + } + + function mapCommand(keys, type, name, args, extra) { + var command = {keys: keys, type: type}; + command[type] = name; + command[type + "Args"] = args; + for (var key in extra) + command[key] = extra[key]; + _mapCommand(command); + } + + // The timeout in milliseconds for the two-character ESC keymap should be + // adjusted according to your typing speed to prevent false positives. + defineOption('insertModeEscKeysTimeout', 200, 'number'); + + CodeMirror.keyMap['vim-insert'] = { + // TODO: override navigation keys so that Esc will cancel automatic + // indentation from o, O, i_ + fallthrough: ['default'], + attach: attachVimMap, + detach: detachVimMap, + call: cmKey + }; + + CodeMirror.keyMap['vim-replace'] = { + 'Backspace': 'goCharLeft', + fallthrough: ['vim-insert'], + attach: attachVimMap, + detach: detachVimMap, + call: cmKey + }; + + function executeMacroRegister(cm, vim, macroModeState, registerName) { + var register = vimGlobalState.registerController.getRegister(registerName); + if (registerName == ':') { + // Read-only register containing last Ex command. + if (register.keyBuffer[0]) { + exCommandDispatcher.processCommand(cm, register.keyBuffer[0]); + } + macroModeState.isPlaying = false; + return; + } + var keyBuffer = register.keyBuffer; + var imc = 0; + macroModeState.isPlaying = true; + macroModeState.replaySearchQueries = register.searchQueries.slice(0); + for (var i = 0; i < keyBuffer.length; i++) { + var text = keyBuffer[i]; + var match, key; + while (text) { + // Pull off one command key, which is either a single character + // or a special sequence wrapped in '<' and '>', e.g. ''. + match = (/<\w+-.+?>|<\w+>|./).exec(text); + key = match[0]; + text = text.substring(match.index + key.length); + CodeMirror.Vim.handleKey(cm, key, 'macro'); + if (vim.insertMode) { + var changes = register.insertModeChanges[imc++].changes; + vimGlobalState.macroModeState.lastInsertModeChanges.changes = + changes; + repeatInsertModeChanges(cm, changes, 1); + exitInsertMode(cm); + } + } + }; + macroModeState.isPlaying = false; + } + + function logKey(macroModeState, key) { + if (macroModeState.isPlaying) { return; } + var registerName = macroModeState.latestRegister; + var register = vimGlobalState.registerController.getRegister(registerName); + if (register) { + register.pushText(key); + } + } + + function logInsertModeChange(macroModeState) { + if (macroModeState.isPlaying) { return; } + var registerName = macroModeState.latestRegister; + var register = vimGlobalState.registerController.getRegister(registerName); + if (register && register.pushInsertModeChanges) { + register.pushInsertModeChanges(macroModeState.lastInsertModeChanges); + } + } + + function logSearchQuery(macroModeState, query) { + if (macroModeState.isPlaying) { return; } + var registerName = macroModeState.latestRegister; + var register = vimGlobalState.registerController.getRegister(registerName); + if (register && register.pushSearchQuery) { + register.pushSearchQuery(query); + } + } + + /** + * Listens for changes made in insert mode. + * Should only be active in insert mode. + */ + function onChange(_cm, changeObj) { + var macroModeState = vimGlobalState.macroModeState; + var lastChange = macroModeState.lastInsertModeChanges; + if (!macroModeState.isPlaying) { + while(changeObj) { + lastChange.expectCursorActivityForChange = true; + if (changeObj.origin == '+input' || changeObj.origin == 'paste' + || changeObj.origin === undefined /* only in testing */) { + var text = changeObj.text.join('\n'); + lastChange.changes.push(text); + } + // Change objects may be chained with next. + changeObj = changeObj.next; + } + } + } + + /** + * Listens for any kind of cursor activity on CodeMirror. + */ + function onCursorActivity(cm) { + var vim = cm.state.vim; + if (vim.insertMode) { + // Tracking cursor activity in insert mode (for macro support). + var macroModeState = vimGlobalState.macroModeState; + if (macroModeState.isPlaying) { return; } + var lastChange = macroModeState.lastInsertModeChanges; + if (lastChange.expectCursorActivityForChange) { + lastChange.expectCursorActivityForChange = false; + } else { + // Cursor moved outside the context of an edit. Reset the change. + lastChange.changes = []; + } + } else if (!cm.curOp.isVimOp) { + handleExternalSelection(cm, vim); + } + if (vim.visualMode) { + updateFakeCursor(cm); + } + } + function updateFakeCursor(cm) { + var vim = cm.state.vim; + var from = clipCursorToContent(cm, copyCursor(vim.sel.head)); + var to = offsetCursor(from, 0, 1); + if (vim.fakeCursor) { + vim.fakeCursor.clear(); + } + vim.fakeCursor = cm.markText(from, to, {className: 'cm-animate-fat-cursor'}); + } + function handleExternalSelection(cm, vim) { + var anchor = cm.getCursor('anchor'); + var head = cm.getCursor('head'); + // Enter or exit visual mode to match mouse selection. + if (vim.visualMode && !cm.somethingSelected()) { + exitVisualMode(cm, false); + } else if (!vim.visualMode && !vim.insertMode && cm.somethingSelected()) { + vim.visualMode = true; + vim.visualLine = false; + CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"}); + } + if (vim.visualMode) { + // Bind CodeMirror selection model to vim selection model. + // Mouse selections are considered visual characterwise. + var headOffset = !cursorIsBefore(head, anchor) ? -1 : 0; + var anchorOffset = cursorIsBefore(head, anchor) ? -1 : 0; + head = offsetCursor(head, 0, headOffset); + anchor = offsetCursor(anchor, 0, anchorOffset); + vim.sel = { + anchor: anchor, + head: head + }; + updateMark(cm, vim, '<', cursorMin(head, anchor)); + updateMark(cm, vim, '>', cursorMax(head, anchor)); + } else if (!vim.insertMode) { + // Reset lastHPos if selection was modified by something outside of vim mode e.g. by mouse. + vim.lastHPos = cm.getCursor().ch; + } + } + + /** Wrapper for special keys pressed in insert mode */ + function InsertModeKey(keyName) { + this.keyName = keyName; + } + + /** + * Handles raw key down events from the text area. + * - Should only be active in insert mode. + * - For recording deletes in insert mode. + */ + function onKeyEventTargetKeyDown(e) { + var macroModeState = vimGlobalState.macroModeState; + var lastChange = macroModeState.lastInsertModeChanges; + var keyName = CodeMirror.keyName(e); + if (!keyName) { return; } + function onKeyFound() { + lastChange.changes.push(new InsertModeKey(keyName)); + return true; + } + if (keyName.indexOf('Delete') != -1 || keyName.indexOf('Backspace') != -1) { + CodeMirror.lookupKey(keyName, 'vim-insert', onKeyFound); + } + } + + /** + * Repeats the last edit, which includes exactly 1 command and at most 1 + * insert. Operator and motion commands are read from lastEditInputState, + * while action commands are read from lastEditActionCommand. + * + * If repeatForInsert is true, then the function was called by + * exitInsertMode to repeat the insert mode changes the user just made. The + * corresponding enterInsertMode call was made with a count. + */ + function repeatLastEdit(cm, vim, repeat, repeatForInsert) { + var macroModeState = vimGlobalState.macroModeState; + macroModeState.isPlaying = true; + var isAction = !!vim.lastEditActionCommand; + var cachedInputState = vim.inputState; + function repeatCommand() { + if (isAction) { + commandDispatcher.processAction(cm, vim, vim.lastEditActionCommand); + } else { + commandDispatcher.evalInput(cm, vim); + } + } + function repeatInsert(repeat) { + if (macroModeState.lastInsertModeChanges.changes.length > 0) { + // For some reason, repeat cw in desktop VIM does not repeat + // insert mode changes. Will conform to that behavior. + repeat = !vim.lastEditActionCommand ? 1 : repeat; + var changeObject = macroModeState.lastInsertModeChanges; + repeatInsertModeChanges(cm, changeObject.changes, repeat); + } + } + vim.inputState = vim.lastEditInputState; + if (isAction && vim.lastEditActionCommand.interlaceInsertRepeat) { + // o and O repeat have to be interlaced with insert repeats so that the + // insertions appear on separate lines instead of the last line. + for (var i = 0; i < repeat; i++) { + repeatCommand(); + repeatInsert(1); + } + } else { + if (!repeatForInsert) { + // Hack to get the cursor to end up at the right place. If I is + // repeated in insert mode repeat, cursor will be 1 insert + // change set left of where it should be. + repeatCommand(); + } + repeatInsert(repeat); + } + vim.inputState = cachedInputState; + if (vim.insertMode && !repeatForInsert) { + // Don't exit insert mode twice. If repeatForInsert is set, then we + // were called by an exitInsertMode call lower on the stack. + exitInsertMode(cm); + } + macroModeState.isPlaying = false; + }; + + function repeatInsertModeChanges(cm, changes, repeat) { + function keyHandler(binding) { + if (typeof binding == 'string') { + CodeMirror.commands[binding](cm); + } else { + binding(cm); + } + return true; + } + var head = cm.getCursor('head'); + var inVisualBlock = vimGlobalState.macroModeState.lastInsertModeChanges.inVisualBlock; + if (inVisualBlock) { + // Set up block selection again for repeating the changes. + var vim = cm.state.vim; + var lastSel = vim.lastSelection; + var offset = getOffset(lastSel.anchor, lastSel.head); + selectForInsert(cm, head, offset.line + 1); + repeat = cm.listSelections().length; + cm.setCursor(head); + } + for (var i = 0; i < repeat; i++) { + if (inVisualBlock) { + cm.setCursor(offsetCursor(head, i, 0)); + } + for (var j = 0; j < changes.length; j++) { + var change = changes[j]; + if (change instanceof InsertModeKey) { + CodeMirror.lookupKey(change.keyName, 'vim-insert', keyHandler); + } else { + var cur = cm.getCursor(); + cm.replaceRange(change, cur, cur); + } + } + } + if (inVisualBlock) { + cm.setCursor(offsetCursor(head, 0, 1)); + } + } + + resetVimGlobalState(); + return vimApi; + }; + // Initialize Vim and make it available as an API. + CodeMirror.Vim = Vim(); +}); +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + CodeMirror.defineOption("scrollPastEnd", false, function(cm, val, old) { + if (old && old != CodeMirror.Init) { + cm.off("change", onChange); + cm.off("refresh", updateBottomMargin); + cm.display.lineSpace.parentNode.style.paddingBottom = ""; + cm.state.scrollPastEndPadding = null; + } + if (val) { + cm.on("change", onChange); + cm.on("refresh", updateBottomMargin); + updateBottomMargin(cm); + } + }); + + function onChange(cm, change) { + if (CodeMirror.changeEnd(change).line == cm.lastLine()) + updateBottomMargin(cm); + } + + function updateBottomMargin(cm) { + var padding = ""; + if (cm.lineCount() > 1) { + var totalH = cm.display.scroller.clientHeight - 30, + lastLineH = cm.getLineHandle(cm.lastLine()).height; + padding = (totalH - lastLineH) + "px"; + } + if (cm.state.scrollPastEndPadding != padding) { + cm.state.scrollPastEndPadding = padding; + cm.display.lineSpace.parentNode.style.paddingBottom = padding; + cm.off("refresh", updateBottomMargin); + cm.setSize(); + cm.on("refresh", updateBottomMargin); + } + } +}); diff --git a/src/commonmark.js b/src/commonmark.js new file mode 100644 index 000000000..7eef3af7b --- /dev/null +++ b/src/commonmark.js @@ -0,0 +1,3339 @@ +!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var o;"undefined"!=typeof window?o=window:"undefined"!=typeof global?o=global:"undefined"!=typeof self&&(o=self),o.commonmark=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o|$)/i, + /^/, + /\?>/, + />/, + /\]\]>/ +]; + +var reThematicBreak = /^(?:(?:\*[ \t]*){3,}|(?:_[ \t]*){3,}|(?:-[ \t]*){3,})[ \t]*$/; + +var reMaybeSpecial = /^[#`~*+_=<>0-9-]/; + +var reNonSpace = /[^ \t\f\v\r\n]/; + +var reBulletListMarker = /^[*+-]/; + +var reOrderedListMarker = /^(\d{1,9})([.)])/; + +var reATXHeadingMarker = /^#{1,6}(?:[ \t]+|$)/; + +var reCodeFence = /^`{3,}(?!.*`)|^~{3,}(?!.*~)/; + +var reClosingCodeFence = /^(?:`{3,}|~{3,})(?= *$)/; + +var reSetextHeadingLine = /^(?:=+|-+) *$/; + +var reLineEnding = /\r\n|\n|\r/; + +// Returns true if string contains only space characters. +var isBlank = function(s) { + return !(reNonSpace.test(s)); +}; + +var isSpaceOrTab = function(c) { + return c === C_SPACE || c === C_TAB; +}; + +var peek = function(ln, pos) { + if (pos < ln.length) { + return ln.charCodeAt(pos); + } else { + return -1; + } +}; + +// DOC PARSER + +// These are methods of a Parser object, defined below. + +// Returns true if block ends with a blank line, descending if needed +// into lists and sublists. +var endsWithBlankLine = function(block) { + while (block) { + if (block._lastLineBlank) { + return true; + } + var t = block.type; + if (t === 'list' || t === 'item') { + block = block._lastChild; + } else { + break; + } + } + return false; +}; + +// Add a line to the block at the tip. We assume the tip +// can accept lines -- that check should be done before calling this. +var addLine = function() { + if (this.partiallyConsumedTab) { + this.offset += 1; // skip over tab + // add space characters: + var charsToTab = 4 - (this.column % 4); + this.tip._string_content += (' '.repeat(charsToTab)); + } + this.tip._string_content += this.currentLine.slice(this.offset) + '\n'; +}; + +// Add block of type tag as a child of the tip. If the tip can't +// accept children, close and finalize it and try its parent, +// and so on til we find a block that can accept children. +var addChild = function(tag, offset) { + while (!this.blocks[this.tip.type].canContain(tag)) { + this.finalize(this.tip, this.lineNumber - 1); + } + + var column_number = offset + 1; // offset 0 = column 1 + var newBlock = new Node(tag, [[this.lineNumber, column_number], [0, 0]]); + newBlock._string_content = ''; + this.tip.appendChild(newBlock); + this.tip = newBlock; + return newBlock; +}; + +// Parse a list marker and return data on the marker (type, +// start, delimiter, bullet character, padding) or null. +var parseListMarker = function(parser, container) { + var rest = parser.currentLine.slice(parser.nextNonspace); + var match; + var nextc; + var spacesStartCol; + var spacesStartOffset; + var data = { type: null, + tight: true, // lists are tight by default + bulletChar: null, + start: null, + delimiter: null, + padding: null, + markerOffset: parser.indent }; + if ((match = rest.match(reBulletListMarker))) { + data.type = 'bullet'; + data.bulletChar = match[0][0]; + + } else if ((match = rest.match(reOrderedListMarker)) && + (container.type !== 'paragraph' || + match[1] === '1')) { + data.type = 'ordered'; + data.start = parseInt(match[1]); + data.delimiter = match[2]; + } else { + return null; + } + // make sure we have spaces after + nextc = peek(parser.currentLine, parser.nextNonspace + match[0].length); + if (!(nextc === -1 || nextc === C_TAB || nextc === C_SPACE)) { + return null; + } + + // if it interrupts paragraph, make sure first line isn't blank + if (container.type === 'paragraph' && !parser.currentLine.slice(parser.nextNonspace + match[0].length).match(reNonSpace)) { + return null; + } + + // we've got a match! advance offset and calculate padding + parser.advanceNextNonspace(); // to start of marker + parser.advanceOffset(match[0].length, true); // to end of marker + spacesStartCol = parser.column; + spacesStartOffset = parser.offset; + do { + parser.advanceOffset(1, true); + nextc = peek(parser.currentLine, parser.offset); + } while (parser.column - spacesStartCol < 5 && + isSpaceOrTab(nextc)); + var blank_item = peek(parser.currentLine, parser.offset) === -1; + var spaces_after_marker = parser.column - spacesStartCol; + if (spaces_after_marker >= 5 || + spaces_after_marker < 1 || + blank_item) { + data.padding = match[0].length + 1; + parser.column = spacesStartCol; + parser.offset = spacesStartOffset; + if (isSpaceOrTab(peek(parser.currentLine, parser.offset))) { + parser.advanceOffset(1, true); + } + } else { + data.padding = match[0].length + spaces_after_marker; + } + return data; +}; + +// Returns true if the two list items are of the same type, +// with the same delimiter and bullet character. This is used +// in agglomerating list items into lists. +var listsMatch = function(list_data, item_data) { + return (list_data.type === item_data.type && + list_data.delimiter === item_data.delimiter && + list_data.bulletChar === item_data.bulletChar); +}; + +// Finalize and close any unmatched blocks. +var closeUnmatchedBlocks = function() { + if (!this.allClosed) { + // finalize any blocks not matched + while (this.oldtip !== this.lastMatchedContainer) { + var parent = this.oldtip._parent; + this.finalize(this.oldtip, this.lineNumber - 1); + this.oldtip = parent; + } + this.allClosed = true; + } +}; + +// 'finalize' is run when the block is closed. +// 'continue' is run to check whether the block is continuing +// at a certain line and offset (e.g. whether a block quote +// contains a `>`. It returns 0 for matched, 1 for not matched, +// and 2 for "we've dealt with this line completely, go to next." +var blocks = { + document: { + continue: function() { return 0; }, + finalize: function() { return; }, + canContain: function(t) { return (t !== 'item'); }, + acceptsLines: false + }, + list: { + continue: function() { return 0; }, + finalize: function(parser, block) { + var item = block._firstChild; + while (item) { + // check for non-final list item ending with blank line: + if (endsWithBlankLine(item) && item._next) { + block._listData.tight = false; + break; + } + // recurse into children of list item, to see if there are + // spaces between any of them: + var subitem = item._firstChild; + while (subitem) { + if (endsWithBlankLine(subitem) && + (item._next || subitem._next)) { + block._listData.tight = false; + break; + } + subitem = subitem._next; + } + item = item._next; + } + }, + canContain: function(t) { return (t === 'item'); }, + acceptsLines: false + }, + block_quote: { + continue: function(parser) { + var ln = parser.currentLine; + if (!parser.indented && + peek(ln, parser.nextNonspace) === C_GREATERTHAN) { + parser.advanceNextNonspace(); + parser.advanceOffset(1, false); + if (isSpaceOrTab(peek(ln, parser.offset))) { + parser.advanceOffset(1, true); + } + } else { + return 1; + } + return 0; + }, + finalize: function() { return; }, + canContain: function(t) { return (t !== 'item'); }, + acceptsLines: false + }, + item: { + continue: function(parser, container) { + if (parser.blank) { + if (container._firstChild == null) { + // Blank line after empty list item + return 1; + } else { + parser.advanceNextNonspace(); + } + } else if (parser.indent >= + container._listData.markerOffset + + container._listData.padding) { + parser.advanceOffset(container._listData.markerOffset + + container._listData.padding, true); + } else { + return 1; + } + return 0; + }, + finalize: function() { return; }, + canContain: function(t) { return (t !== 'item'); }, + acceptsLines: false + }, + heading: { + continue: function() { + // a heading can never container > 1 line, so fail to match: + return 1; + }, + finalize: function() { return; }, + canContain: function() { return false; }, + acceptsLines: false + }, + thematic_break: { + continue: function() { + // a thematic break can never container > 1 line, so fail to match: + return 1; + }, + finalize: function() { return; }, + canContain: function() { return false; }, + acceptsLines: false + }, + code_block: { + continue: function(parser, container) { + var ln = parser.currentLine; + var indent = parser.indent; + if (container._isFenced) { // fenced + var match = (indent <= 3 && + ln.charAt(parser.nextNonspace) === container._fenceChar && + ln.slice(parser.nextNonspace).match(reClosingCodeFence)); + if (match && match[0].length >= container._fenceLength) { + // closing fence - we're at end of line, so we can return + parser.finalize(container, parser.lineNumber); + return 2; + } else { + // skip optional spaces of fence offset + var i = container._fenceOffset; + while (i > 0 && isSpaceOrTab(peek(ln, parser.offset))) { + parser.advanceOffset(1, true); + i--; + } + } + } else { // indented + if (indent >= CODE_INDENT) { + parser.advanceOffset(CODE_INDENT, true); + } else if (parser.blank) { + parser.advanceNextNonspace(); + } else { + return 1; + } + } + return 0; + }, + finalize: function(parser, block) { + if (block._isFenced) { // fenced + // first line becomes info string + var content = block._string_content; + var newlinePos = content.indexOf('\n'); + var firstLine = content.slice(0, newlinePos); + var rest = content.slice(newlinePos + 1); + block.info = unescapeString(firstLine.trim()); + block._literal = rest; + } else { // indented + block._literal = block._string_content.replace(/(\n *)+$/, '\n'); + } + block._string_content = null; // allow GC + }, + canContain: function() { return false; }, + acceptsLines: true + }, + html_block: { + continue: function(parser, container) { + return ((parser.blank && + (container._htmlBlockType === 6 || + container._htmlBlockType === 7)) ? 1 : 0); + }, + finalize: function(parser, block) { + block._literal = block._string_content.replace(/(\n *)+$/, ''); + block._string_content = null; // allow GC + }, + canContain: function() { return false; }, + acceptsLines: true + }, + paragraph: { + continue: function(parser) { + return (parser.blank ? 1 : 0); + }, + finalize: function(parser, block) { + var pos; + var hasReferenceDefs = false; + + // try parsing the beginning as link reference definitions: + while (peek(block._string_content, 0) === C_OPEN_BRACKET && + (pos = + parser.inlineParser.parseReference(block._string_content, + parser.refmap))) { + block._string_content = block._string_content.slice(pos); + hasReferenceDefs = true; + } + if (hasReferenceDefs && isBlank(block._string_content)) { + block.unlink(); + } + }, + canContain: function() { return false; }, + acceptsLines: true + } +}; + +// block start functions. Return values: +// 0 = no match +// 1 = matched container, keep going +// 2 = matched leaf, no more block starts +var blockStarts = [ + // block quote + function(parser) { + if (!parser.indented && + peek(parser.currentLine, parser.nextNonspace) === C_GREATERTHAN) { + parser.advanceNextNonspace(); + parser.advanceOffset(1, false); + // optional following space + if (isSpaceOrTab(peek(parser.currentLine, parser.offset))) { + parser.advanceOffset(1, true); + } + parser.closeUnmatchedBlocks(); + parser.addChild('block_quote', parser.nextNonspace); + return 1; + } else { + return 0; + } + }, + + // ATX heading + function(parser) { + var match; + if (!parser.indented && + (match = parser.currentLine.slice(parser.nextNonspace).match(reATXHeadingMarker))) { + parser.advanceNextNonspace(); + parser.advanceOffset(match[0].length, false); + parser.closeUnmatchedBlocks(); + var container = parser.addChild('heading', parser.nextNonspace); + container.level = match[0].trim().length; // number of #s + // remove trailing ###s: + container._string_content = + parser.currentLine.slice(parser.offset).replace(/^ *#+ *$/, '').replace(/ +#+ *$/, ''); + parser.advanceOffset(parser.currentLine.length - parser.offset); + return 2; + } else { + return 0; + } + }, + + // Fenced code block + function(parser) { + var match; + if (!parser.indented && + (match = parser.currentLine.slice(parser.nextNonspace).match(reCodeFence))) { + var fenceLength = match[0].length; + parser.closeUnmatchedBlocks(); + var container = parser.addChild('code_block', parser.nextNonspace); + container._isFenced = true; + container._fenceLength = fenceLength; + container._fenceChar = match[0][0]; + container._fenceOffset = parser.indent; + parser.advanceNextNonspace(); + parser.advanceOffset(fenceLength, false); + return 2; + } else { + return 0; + } + }, + + // HTML block + function(parser, container) { + if (!parser.indented && + peek(parser.currentLine, parser.nextNonspace) === C_LESSTHAN) { + var s = parser.currentLine.slice(parser.nextNonspace); + var blockType; + + for (blockType = 1; blockType <= 7; blockType++) { + if (reHtmlBlockOpen[blockType].test(s) && + (blockType < 7 || + container.type !== 'paragraph')) { + parser.closeUnmatchedBlocks(); + // We don't adjust parser.offset; + // spaces are part of the HTML block: + var b = parser.addChild('html_block', + parser.offset); + b._htmlBlockType = blockType; + return 2; + } + } + } + + return 0; + + }, + + // Setext heading + function(parser, container) { + var match; + if (!parser.indented && + container.type === 'paragraph' && + ((match = parser.currentLine.slice(parser.nextNonspace).match(reSetextHeadingLine)))) { + parser.closeUnmatchedBlocks(); + var heading = new Node('heading', container.sourcepos); + heading.level = match[0][0] === '=' ? 1 : 2; + heading._string_content = container._string_content; + container.insertAfter(heading); + container.unlink(); + parser.tip = heading; + parser.advanceOffset(parser.currentLine.length - parser.offset, false); + return 2; + } else { + return 0; + } + }, + + // thematic break + function(parser) { + if (!parser.indented && + reThematicBreak.test(parser.currentLine.slice(parser.nextNonspace))) { + parser.closeUnmatchedBlocks(); + parser.addChild('thematic_break', parser.nextNonspace); + parser.advanceOffset(parser.currentLine.length - parser.offset, false); + return 2; + } else { + return 0; + } + }, + + // list item + function(parser, container) { + var data; + + if ((!parser.indented || container.type === 'list') + && (data = parseListMarker(parser, container))) { + parser.closeUnmatchedBlocks(); + + // add the list if needed + if (parser.tip.type !== 'list' || + !(listsMatch(container._listData, data))) { + container = parser.addChild('list', parser.nextNonspace); + container._listData = data; + } + + // add the list item + container = parser.addChild('item', parser.nextNonspace); + container._listData = data; + return 1; + } else { + return 0; + } + }, + + // indented code block + function(parser) { + if (parser.indented && + parser.tip.type !== 'paragraph' && + !parser.blank) { + // indented code + parser.advanceOffset(CODE_INDENT, true); + parser.closeUnmatchedBlocks(); + parser.addChild('code_block', parser.offset); + return 2; + } else { + return 0; + } + } + +]; + +var advanceOffset = function(count, columns) { + var currentLine = this.currentLine; + var charsToTab, charsToAdvance; + var c; + while (count > 0 && (c = currentLine[this.offset])) { + if (c === '\t') { + charsToTab = 4 - (this.column % 4); + if (columns) { + this.partiallyConsumedTab = charsToTab > count; + charsToAdvance = charsToTab > count ? count : charsToTab; + this.column += charsToAdvance; + this.offset += this.partiallyConsumedTab ? 0 : 1; + count -= charsToAdvance; + } else { + this.partiallyConsumedTab = false; + this.column += charsToTab; + this.offset += 1; + count -= 1; + } + } else { + this.partiallyConsumedTab = false; + this.offset += 1; + this.column += 1; // assume ascii; block starts are ascii + count -= 1; + } + } +}; + +var advanceNextNonspace = function() { + this.offset = this.nextNonspace; + this.column = this.nextNonspaceColumn; + this.partiallyConsumedTab = false; +}; + +var findNextNonspace = function() { + var currentLine = this.currentLine; + var i = this.offset; + var cols = this.column; + var c; + + while ((c = currentLine.charAt(i)) !== '') { + if (c === ' ') { + i++; + cols++; + } else if (c === '\t') { + i++; + cols += (4 - (cols % 4)); + } else { + break; + } + } + this.blank = (c === '\n' || c === '\r' || c === ''); + this.nextNonspace = i; + this.nextNonspaceColumn = cols; + this.indent = this.nextNonspaceColumn - this.column; + this.indented = this.indent >= CODE_INDENT; +}; + +// Analyze a line of text and update the document appropriately. +// We parse markdown text by calling this on each line of input, +// then finalizing the document. +var incorporateLine = function(ln) { + var all_matched = true; + var t; + + var container = this.doc; + this.oldtip = this.tip; + this.offset = 0; + this.column = 0; + this.blank = false; + this.partiallyConsumedTab = false; + this.lineNumber += 1; + + // replace NUL characters for security + if (ln.indexOf('\u0000') !== -1) { + ln = ln.replace(/\0/g, '\uFFFD'); + } + + this.currentLine = ln; + + // For each containing block, try to parse the associated line start. + // Bail out on failure: container will point to the last matching block. + // Set all_matched to false if not all containers match. + var lastChild; + while ((lastChild = container._lastChild) && lastChild._open) { + container = lastChild; + + this.findNextNonspace(); + + switch (this.blocks[container.type].continue(this, container)) { + case 0: // we've matched, keep going + break; + case 1: // we've failed to match a block + all_matched = false; + break; + case 2: // we've hit end of line for fenced code close and can return + this.lastLineLength = ln.length; + return; + default: + throw 'continue returned illegal value, must be 0, 1, or 2'; + } + if (!all_matched) { + container = container._parent; // back up to last matching block + break; + } + } + + this.allClosed = (container === this.oldtip); + this.lastMatchedContainer = container; + + var matchedLeaf = container.type !== 'paragraph' && + blocks[container.type].acceptsLines; + var starts = this.blockStarts; + var startsLen = starts.length; + // Unless last matched container is a code block, try new container starts, + // adding children to the last matched container: + while (!matchedLeaf) { + + this.findNextNonspace(); + + // this is a little performance optimization: + if (!this.indented && + !reMaybeSpecial.test(ln.slice(this.nextNonspace))) { + this.advanceNextNonspace(); + break; + } + + var i = 0; + while (i < startsLen) { + var res = starts[i](this, container); + if (res === 1) { + container = this.tip; + break; + } else if (res === 2) { + container = this.tip; + matchedLeaf = true; + break; + } else { + i++; + } + } + + if (i === startsLen) { // nothing matched + this.advanceNextNonspace(); + break; + } + } + + // What remains at the offset is a text line. Add the text to the + // appropriate container. + + // First check for a lazy paragraph continuation: + if (!this.allClosed && !this.blank && + this.tip.type === 'paragraph') { + // lazy paragraph continuation + this.addLine(); + + } else { // not a lazy continuation + + // finalize any blocks not matched + this.closeUnmatchedBlocks(); + if (this.blank && container.lastChild) { + container.lastChild._lastLineBlank = true; + } + + t = container.type; + + // Block quote lines are never blank as they start with > + // and we don't count blanks in fenced code for purposes of tight/loose + // lists or breaking out of lists. We also don't set _lastLineBlank + // on an empty list item, or if we just closed a fenced block. + var lastLineBlank = this.blank && + !(t === 'block_quote' || + (t === 'code_block' && container._isFenced) || + (t === 'item' && + !container._firstChild && + container.sourcepos[0][0] === this.lineNumber)); + + // propagate lastLineBlank up through parents: + var cont = container; + while (cont) { + cont._lastLineBlank = lastLineBlank; + cont = cont._parent; + } + + if (this.blocks[t].acceptsLines) { + this.addLine(); + // if HtmlBlock, check for end condition + if (t === 'html_block' && + container._htmlBlockType >= 1 && + container._htmlBlockType <= 5 && + reHtmlBlockClose[container._htmlBlockType].test(this.currentLine.slice(this.offset))) { + this.finalize(container, this.lineNumber); + } + + } else if (this.offset < ln.length && !this.blank) { + // create paragraph container for line + container = this.addChild('paragraph', this.offset); + this.advanceNextNonspace(); + this.addLine(); + } + } + this.lastLineLength = ln.length; +}; + +// Finalize a block. Close it and do any necessary postprocessing, +// e.g. creating string_content from strings, setting the 'tight' +// or 'loose' status of a list, and parsing the beginnings +// of paragraphs for reference definitions. Reset the tip to the +// parent of the closed block. +var finalize = function(block, lineNumber) { + var above = block._parent; + block._open = false; + block.sourcepos[1] = [lineNumber, this.lastLineLength]; + + this.blocks[block.type].finalize(this, block); + + this.tip = above; +}; + +// Walk through a block & children recursively, parsing string content +// into inline content where appropriate. +var processInlines = function(block) { + var node, event, t; + var walker = block.walker(); + this.inlineParser.refmap = this.refmap; + this.inlineParser.options = this.options; + while ((event = walker.next())) { + node = event.node; + t = node.type; + if (!event.entering && (t === 'paragraph' || t === 'heading')) { + this.inlineParser.parse(node); + } + } +}; + +var Document = function() { + var doc = new Node('document', [[1, 1], [0, 0]]); + return doc; +}; + +// The main parsing function. Returns a parsed document AST. +var parse = function(input) { + this.doc = new Document(); + this.tip = this.doc; + this.refmap = {}; + this.lineNumber = 0; + this.lastLineLength = 0; + this.offset = 0; + this.column = 0; + this.lastMatchedContainer = this.doc; + this.currentLine = ""; + if (this.options.time) { console.time("preparing input"); } + var lines = input.split(reLineEnding); + var len = lines.length; + if (input.charCodeAt(input.length - 1) === C_NEWLINE) { + // ignore last blank line created by final newline + len -= 1; + } + if (this.options.time) { console.timeEnd("preparing input"); } + if (this.options.time) { console.time("block parsing"); } + for (var i = 0; i < len; i++) { + this.incorporateLine(lines[i]); + } + while (this.tip) { + this.finalize(this.tip, len); + } + if (this.options.time) { console.timeEnd("block parsing"); } + if (this.options.time) { console.time("inline parsing"); } + this.processInlines(this.doc); + if (this.options.time) { console.timeEnd("inline parsing"); } + return this.doc; +}; + + +// The Parser object. +function Parser(options){ + return { + doc: new Document(), + blocks: blocks, + blockStarts: blockStarts, + tip: this.doc, + oldtip: this.doc, + currentLine: "", + lineNumber: 0, + offset: 0, + column: 0, + nextNonspace: 0, + nextNonspaceColumn: 0, + indent: 0, + indented: false, + blank: false, + partiallyConsumedTab: false, + allClosed: true, + lastMatchedContainer: this.doc, + refmap: {}, + lastLineLength: 0, + inlineParser: new InlineParser(options), + findNextNonspace: findNextNonspace, + advanceOffset: advanceOffset, + advanceNextNonspace: advanceNextNonspace, + addLine: addLine, + addChild: addChild, + incorporateLine: incorporateLine, + finalize: finalize, + processInlines: processInlines, + closeUnmatchedBlocks: closeUnmatchedBlocks, + parse: parse, + options: options || {} + }; +} + +module.exports = Parser; + +},{"./common":2,"./inlines":5,"./node":6}],2:[function(require,module,exports){ +"use strict"; + +var encode = require('mdurl/encode'); +var decode = require('mdurl/decode'); + +var C_BACKSLASH = 92; + +var decodeHTML = require('entities').decodeHTML; + +var ENTITY = "&(?:#x[a-f0-9]{1,8}|#[0-9]{1,8}|[a-z][a-z0-9]{1,31});"; + +var TAGNAME = '[A-Za-z][A-Za-z0-9-]*'; +var ATTRIBUTENAME = '[a-zA-Z_:][a-zA-Z0-9:._-]*'; +var UNQUOTEDVALUE = "[^\"'=<>`\\x00-\\x20]+"; +var SINGLEQUOTEDVALUE = "'[^']*'"; +var DOUBLEQUOTEDVALUE = '"[^"]*"'; +var ATTRIBUTEVALUE = "(?:" + UNQUOTEDVALUE + "|" + SINGLEQUOTEDVALUE + "|" + DOUBLEQUOTEDVALUE + ")"; +var ATTRIBUTEVALUESPEC = "(?:" + "\\s*=" + "\\s*" + ATTRIBUTEVALUE + ")"; +var ATTRIBUTE = "(?:" + "\\s+" + ATTRIBUTENAME + ATTRIBUTEVALUESPEC + "?)"; +var OPENTAG = "<" + TAGNAME + ATTRIBUTE + "*" + "\\s*/?>"; +var CLOSETAG = "]"; +var HTMLCOMMENT = "|"; +var PROCESSINGINSTRUCTION = "[<][?].*?[?][>]"; +var DECLARATION = "]*>"; +var CDATA = ""; +var HTMLTAG = "(?:" + OPENTAG + "|" + CLOSETAG + "|" + HTMLCOMMENT + "|" + + PROCESSINGINSTRUCTION + "|" + DECLARATION + "|" + CDATA + ")"; +var reHtmlTag = new RegExp('^' + HTMLTAG, 'i'); + +var reBackslashOrAmp = /[\\&]/; + +var ESCAPABLE = '[!"#$%&\'()*+,./:;<=>?@[\\\\\\]^_`{|}~-]'; + +var reEntityOrEscapedChar = new RegExp('\\\\' + ESCAPABLE + '|' + ENTITY, 'gi'); + +var XMLSPECIAL = '[&<>"]'; + +var reXmlSpecial = new RegExp(XMLSPECIAL, 'g'); + +var reXmlSpecialOrEntity = new RegExp(ENTITY + '|' + XMLSPECIAL, 'gi'); + +var unescapeChar = function(s) { + if (s.charCodeAt(0) === C_BACKSLASH) { + return s.charAt(1); + } else { + return decodeHTML(s); + } +}; + +// Replace entities and backslash escapes with literal characters. +var unescapeString = function(s) { + if (reBackslashOrAmp.test(s)) { + return s.replace(reEntityOrEscapedChar, unescapeChar); + } else { + return s; + } +}; + +var normalizeURI = function(uri) { + try { + return encode(decode(uri)); + } + catch(err) { + return uri; + } +}; + +var replaceUnsafeChar = function(s) { + switch (s) { + case '&': + return '&'; + case '<': + return '<'; + case '>': + return '>'; + case '"': + return '"'; + default: + return s; + } +}; + +var escapeXml = function(s, preserve_entities) { + if (reXmlSpecial.test(s)) { + if (preserve_entities) { + return s.replace(reXmlSpecialOrEntity, replaceUnsafeChar); + } else { + return s.replace(reXmlSpecial, replaceUnsafeChar); + } + } else { + return s; + } +}; + +module.exports = { unescapeString: unescapeString, + normalizeURI: normalizeURI, + escapeXml: escapeXml, + reHtmlTag: reHtmlTag, + OPENTAG: OPENTAG, + CLOSETAG: CLOSETAG, + ENTITY: ENTITY, + ESCAPABLE: ESCAPABLE + }; + +},{"entities":11,"mdurl/decode":19,"mdurl/encode":20}],3:[function(require,module,exports){ +"use strict"; + +// derived from https://github.com/mathiasbynens/String.fromCodePoint +/*! http://mths.be/fromcodepoint v0.2.1 by @mathias */ +if (String.fromCodePoint) { + module.exports = function (_) { + try { + return String.fromCodePoint(_); + } catch (e) { + if (e instanceof RangeError) { + return String.fromCharCode(0xFFFD); + } + throw e; + } + }; + +} else { + + var stringFromCharCode = String.fromCharCode; + var floor = Math.floor; + var fromCodePoint = function() { + var MAX_SIZE = 0x4000; + var codeUnits = []; + var highSurrogate; + var lowSurrogate; + var index = -1; + var length = arguments.length; + if (!length) { + return ''; + } + var result = ''; + while (++index < length) { + var codePoint = Number(arguments[index]); + if ( + !isFinite(codePoint) || // `NaN`, `+Infinity`, or `-Infinity` + codePoint < 0 || // not a valid Unicode code point + codePoint > 0x10FFFF || // not a valid Unicode code point + floor(codePoint) !== codePoint // not an integer + ) { + return String.fromCharCode(0xFFFD); + } + if (codePoint <= 0xFFFF) { // BMP code point + codeUnits.push(codePoint); + } else { // Astral code point; split in surrogate halves + // http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae + codePoint -= 0x10000; + highSurrogate = (codePoint >> 10) + 0xD800; + lowSurrogate = (codePoint % 0x400) + 0xDC00; + codeUnits.push(highSurrogate, lowSurrogate); + } + if (index + 1 === length || codeUnits.length > MAX_SIZE) { + result += stringFromCharCode.apply(null, codeUnits); + codeUnits.length = 0; + } + } + return result; + }; + module.exports = fromCodePoint; +} + +},{}],4:[function(require,module,exports){ +"use strict"; + +// commonmark.js - CommomMark in JavaScript +// Copyright (C) 2014 John MacFarlane +// License: BSD3. + +// Basic usage: +// +// var commonmark = require('commonmark'); +// var parser = new commonmark.Parser(); +// var renderer = new commonmark.HtmlRenderer(); +// console.log(renderer.render(parser.parse('Hello *world*'))); + +module.exports.version = '0.26.0'; +module.exports.Node = require('./node'); +module.exports.Parser = require('./blocks'); +// module.exports.HtmlRenderer = require('./html'); +module.exports.HtmlRenderer = require('./render/html'); +module.exports.XmlRenderer = require('./xml'); + +},{"./blocks":1,"./node":6,"./render/html":8,"./xml":10}],5:[function(require,module,exports){ +"use strict"; + +var Node = require('./node'); +var common = require('./common'); +var normalizeReference = require('./normalize-reference'); + +var normalizeURI = common.normalizeURI; +var unescapeString = common.unescapeString; +var fromCodePoint = require('./from-code-point.js'); +var decodeHTML = require('entities').decodeHTML; +require('string.prototype.repeat'); // Polyfill for String.prototype.repeat + +// Constants for character codes: + +var C_NEWLINE = 10; +var C_ASTERISK = 42; +var C_UNDERSCORE = 95; +var C_BACKTICK = 96; +var C_OPEN_BRACKET = 91; +var C_CLOSE_BRACKET = 93; +var C_LESSTHAN = 60; +var C_BANG = 33; +var C_BACKSLASH = 92; +var C_AMPERSAND = 38; +var C_OPEN_PAREN = 40; +var C_CLOSE_PAREN = 41; +var C_COLON = 58; +var C_SINGLEQUOTE = 39; +var C_DOUBLEQUOTE = 34; + +// Some regexps used in inline parser: + +var ESCAPABLE = common.ESCAPABLE; +var ESCAPED_CHAR = '\\\\' + ESCAPABLE; +var REG_CHAR = '[^\\\\()\\x00-\\x20]'; +var IN_PARENS_NOSP = '\\((' + REG_CHAR + '|' + ESCAPED_CHAR + '|\\\\)*\\)'; + +var ENTITY = common.ENTITY; +var reHtmlTag = common.reHtmlTag; + +var rePunctuation = new RegExp(/^[\u2000-\u206F\u2E00-\u2E7F\\'!"#\$%&\(\)\*\+,\-\.\/:;<=>\?@\[\]\^_`\{\|\}~]/); + +var reLinkTitle = new RegExp( + '^(?:"(' + ESCAPED_CHAR + '|[^"\\x00])*"' + + '|' + + '\'(' + ESCAPED_CHAR + '|[^\'\\x00])*\'' + + '|' + + '\\((' + ESCAPED_CHAR + '|[^)\\x00])*\\))'); + +var reLinkDestinationBraces = new RegExp( + '^(?:[<](?:[^ <>\\t\\n\\\\\\x00]' + '|' + ESCAPED_CHAR + '|' + '\\\\)*[>])'); + +var reLinkDestination = new RegExp( + '^(?:' + REG_CHAR + '+|' + ESCAPED_CHAR + '|\\\\|' + IN_PARENS_NOSP + ')*'); + +var reEscapable = new RegExp('^' + ESCAPABLE); + +var reEntityHere = new RegExp('^' + ENTITY, 'i'); + +var reTicks = /`+/; + +var reTicksHere = /^`+/; + +var reEllipses = /\.\.\./g; + +var reDash = /--+/g; + +var reEmailAutolink = /^<([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>/; + +var reAutolink = /^<[A-Za-z][A-Za-z0-9.+-]{1,31}:[^<>\x00-\x20]*>/i; + +var reSpnl = /^ *(?:\n *)?/; + +var reWhitespaceChar = /^\s/; + +var reWhitespace = /\s+/g; + +var reFinalSpace = / *$/; + +var reInitialSpace = /^ */; + +var reSpaceAtEndOfLine = /^ *(?:\n|$)/; + +var reLinkLabel = new RegExp('^\\[(?:[^\\\\\\[\\]]|' + ESCAPED_CHAR + + '|\\\\){0,1000}\\]'); + +// Matches a string of non-special characters. +var reMain = /^[^\n`\[\]\\!<&*_'"]+/m; + +var text = function(s) { + var node = new Node('text'); + node._literal = s; + return node; +}; + +// INLINE PARSER + +// These are methods of an InlineParser object, defined below. +// An InlineParser keeps track of a subject (a string to be +// parsed) and a position in that subject. + +// If re matches at current position in the subject, advance +// position in subject and return the match; otherwise return null. +var match = function(re) { + var m = re.exec(this.subject.slice(this.pos)); + if (m === null) { + return null; + } else { + this.pos += m.index + m[0].length; + return m[0]; + } +}; + +// Returns the code for the character at the current subject position, or -1 +// there are no more characters. +var peek = function() { + if (this.pos < this.subject.length) { + return this.subject.charCodeAt(this.pos); + } else { + return -1; + } +}; + +// Parse zero or more space characters, including at most one newline +var spnl = function() { + this.match(reSpnl); + return true; +}; + +// All of the parsers below try to match something at the current position +// in the subject. If they succeed in matching anything, they +// return the inline matched, advancing the subject. + +// Attempt to parse backticks, adding either a backtick code span or a +// literal sequence of backticks. +var parseBackticks = function(block) { + var ticks = this.match(reTicksHere); + if (ticks === null) { + return false; + } + var afterOpenTicks = this.pos; + var matched; + var node; + while ((matched = this.match(reTicks)) !== null) { + if (matched === ticks) { + node = new Node('code'); + node._literal = this.subject.slice(afterOpenTicks, + this.pos - ticks.length) + .trim().replace(reWhitespace, ' '); + block.appendChild(node); + return true; + } + } + // If we got here, we didn't match a closing backtick sequence. + this.pos = afterOpenTicks; + block.appendChild(text(ticks)); + return true; +}; + +// Parse a backslash-escaped special character, adding either the escaped +// character, a hard line break (if the backslash is followed by a newline), +// or a literal backslash to the block's children. Assumes current character +// is a backslash. +var parseBackslash = function(block) { + var subj = this.subject; + var node; + this.pos += 1; + if (this.peek() === C_NEWLINE) { + this.pos += 1; + node = new Node('linebreak'); + block.appendChild(node); + } else if (reEscapable.test(subj.charAt(this.pos))) { + block.appendChild(text(subj.charAt(this.pos))); + this.pos += 1; + } else { + block.appendChild(text('\\')); + } + return true; +}; + +// Attempt to parse an autolink (URL or email in pointy brackets). +var parseAutolink = function(block) { + var m; + var dest; + var node; + if ((m = this.match(reEmailAutolink))) { + dest = m.slice(1, m.length - 1); + node = new Node('link'); + node._destination = normalizeURI('mailto:' + dest); + node._title = ''; + node.appendChild(text(dest)); + block.appendChild(node); + return true; + } else if ((m = this.match(reAutolink))) { + dest = m.slice(1, m.length - 1); + node = new Node('link'); + node._destination = normalizeURI(dest); + node._title = ''; + node.appendChild(text(dest)); + block.appendChild(node); + return true; + } else { + return false; + } +}; + +// Attempt to parse a raw HTML tag. +var parseHtmlTag = function(block) { + var m = this.match(reHtmlTag); + if (m === null) { + return false; + } else { + var node = new Node('html_inline'); + node._literal = m; + block.appendChild(node); + return true; + } +}; + +// Scan a sequence of characters with code cc, and return information about +// the number of delimiters and whether they are positioned such that +// they can open and/or close emphasis or strong emphasis. A utility +// function for strong/emph parsing. +var scanDelims = function(cc) { + var numdelims = 0; + var char_before, char_after, cc_after; + var startpos = this.pos; + var left_flanking, right_flanking, can_open, can_close; + var after_is_whitespace, after_is_punctuation, before_is_whitespace, before_is_punctuation; + + if (cc === C_SINGLEQUOTE || cc === C_DOUBLEQUOTE) { + numdelims++; + this.pos++; + } else { + while (this.peek() === cc) { + numdelims++; + this.pos++; + } + } + + if (numdelims === 0) { + return null; + } + + char_before = startpos === 0 ? '\n' : this.subject.charAt(startpos - 1); + + cc_after = this.peek(); + if (cc_after === -1) { + char_after = '\n'; + } else { + char_after = fromCodePoint(cc_after); + } + + after_is_whitespace = reWhitespaceChar.test(char_after); + after_is_punctuation = rePunctuation.test(char_after); + before_is_whitespace = reWhitespaceChar.test(char_before); + before_is_punctuation = rePunctuation.test(char_before); + + left_flanking = !after_is_whitespace && + !(after_is_punctuation && !before_is_whitespace && !before_is_punctuation); + right_flanking = !before_is_whitespace && + !(before_is_punctuation && !after_is_whitespace && !after_is_punctuation); + if (cc === C_UNDERSCORE) { + can_open = left_flanking && + (!right_flanking || before_is_punctuation); + can_close = right_flanking && + (!left_flanking || after_is_punctuation); + } else if (cc === C_SINGLEQUOTE || cc === C_DOUBLEQUOTE) { + can_open = left_flanking && !right_flanking; + can_close = right_flanking; + } else { + can_open = left_flanking; + can_close = right_flanking; + } + this.pos = startpos; + return { numdelims: numdelims, + can_open: can_open, + can_close: can_close }; +}; + +// Handle a delimiter marker for emphasis or a quote. +var handleDelim = function(cc, block) { + var res = this.scanDelims(cc); + if (!res) { + return false; + } + var numdelims = res.numdelims; + var startpos = this.pos; + var contents; + + this.pos += numdelims; + if (cc === C_SINGLEQUOTE) { + contents = "\u2019"; + } else if (cc === C_DOUBLEQUOTE) { + contents = "\u201C"; + } else { + contents = this.subject.slice(startpos, this.pos); + } + var node = text(contents); + block.appendChild(node); + + // Add entry to stack for this opener + this.delimiters = { cc: cc, + numdelims: numdelims, + node: node, + previous: this.delimiters, + next: null, + can_open: res.can_open, + can_close: res.can_close }; + if (this.delimiters.previous !== null) { + this.delimiters.previous.next = this.delimiters; + } + + return true; + +}; + +var removeDelimiter = function(delim) { + if (delim.previous !== null) { + delim.previous.next = delim.next; + } + if (delim.next === null) { + // top of stack + this.delimiters = delim.previous; + } else { + delim.next.previous = delim.previous; + } +}; + +var removeDelimitersBetween = function(bottom, top) { + if (bottom.next !== top) { + bottom.next = top; + top.previous = bottom; + } +}; + +var processEmphasis = function(stack_bottom) { + var opener, closer, old_closer; + var opener_inl, closer_inl; + var tempstack; + var use_delims; + var tmp, next; + var opener_found; + var openers_bottom = []; + var odd_match = false; + + openers_bottom[C_UNDERSCORE] = stack_bottom; + openers_bottom[C_ASTERISK] = stack_bottom; + openers_bottom[C_SINGLEQUOTE] = stack_bottom; + openers_bottom[C_DOUBLEQUOTE] = stack_bottom; + + // find first closer above stack_bottom: + closer = this.delimiters; + while (closer !== null && closer.previous !== stack_bottom) { + closer = closer.previous; + } + // move forward, looking for closers, and handling each + while (closer !== null) { + var closercc = closer.cc; + if (!closer.can_close) { + closer = closer.next; + } else { + // found emphasis closer. now look back for first matching opener: + opener = closer.previous; + opener_found = false; + while (opener !== null && opener !== stack_bottom && + opener !== openers_bottom[closercc]) { + odd_match = (closer.can_open || opener.can_close) && + (opener.numdelims + closer.numdelims) % 3 === 0; + if (opener.cc === closer.cc && opener.can_open && !odd_match) { + opener_found = true; + break; + } + opener = opener.previous; + } + old_closer = closer; + + if (closercc === C_ASTERISK || closercc === C_UNDERSCORE) { + if (!opener_found) { + closer = closer.next; + } else { + // calculate actual number of delimiters used from closer + if (closer.numdelims < 3 || opener.numdelims < 3) { + use_delims = closer.numdelims <= opener.numdelims ? + closer.numdelims : opener.numdelims; + } else { + use_delims = closer.numdelims % 2 === 0 ? 2 : 1; + } + + opener_inl = opener.node; + closer_inl = closer.node; + + // remove used delimiters from stack elts and inlines + opener.numdelims -= use_delims; + closer.numdelims -= use_delims; + opener_inl._literal = + opener_inl._literal.slice(0, + opener_inl._literal.length - use_delims); + closer_inl._literal = + closer_inl._literal.slice(0, + closer_inl._literal.length - use_delims); + + // build contents for new emph element + var emph = new Node(use_delims === 1 ? 'emph' : 'strong'); + + tmp = opener_inl._next; + while (tmp && tmp !== closer_inl) { + next = tmp._next; + tmp.unlink(); + emph.appendChild(tmp); + tmp = next; + } + + opener_inl.insertAfter(emph); + + // remove elts between opener and closer in delimiters stack + removeDelimitersBetween(opener, closer); + + // if opener has 0 delims, remove it and the inline + if (opener.numdelims === 0) { + opener_inl.unlink(); + this.removeDelimiter(opener); + } + + if (closer.numdelims === 0) { + closer_inl.unlink(); + tempstack = closer.next; + this.removeDelimiter(closer); + closer = tempstack; + } + + } + + } else if (closercc === C_SINGLEQUOTE) { + closer.node._literal = "\u2019"; + if (opener_found) { + opener.node._literal = "\u2018"; + } + closer = closer.next; + + } else if (closercc === C_DOUBLEQUOTE) { + closer.node._literal = "\u201D"; + if (opener_found) { + opener.node.literal = "\u201C"; + } + closer = closer.next; + + } + if (!opener_found && !odd_match) { + // Set lower bound for future searches for openers: + // We don't do this with odd_match because a ** + // that doesn't match an earlier * might turn into + // an opener, and the * might be matched by something + // else. + openers_bottom[closercc] = old_closer.previous; + if (!old_closer.can_open) { + // We can remove a closer that can't be an opener, + // once we've seen there's no matching opener: + this.removeDelimiter(old_closer); + } + } + } + + } + + // remove all delimiters + while (this.delimiters !== null && this.delimiters !== stack_bottom) { + this.removeDelimiter(this.delimiters); + } +}; + +// Attempt to parse link title (sans quotes), returning the string +// or null if no match. +var parseLinkTitle = function() { + var title = this.match(reLinkTitle); + if (title === null) { + return null; + } else { + // chop off quotes from title and unescape: + return unescapeString(title.substr(1, title.length - 2)); + } +}; + +// Attempt to parse link destination, returning the string or +// null if no match. +var parseLinkDestination = function() { + var res = this.match(reLinkDestinationBraces); + if (res === null) { + res = this.match(reLinkDestination); + if (res === null) { + return null; + } else { + return normalizeURI(unescapeString(res)); + } + } else { // chop off surrounding <..>: + return normalizeURI(unescapeString(res.substr(1, res.length - 2))); + } +}; + +// Attempt to parse a link label, returning number of characters parsed. +var parseLinkLabel = function() { + var m = this.match(reLinkLabel); + if (m === null || m.length > 1001) { + return 0; + } else { + return m.length; + } +}; + +// Add open bracket to delimiter stack and add a text node to block's children. +var parseOpenBracket = function(block) { + var startpos = this.pos; + this.pos += 1; + + var node = text('['); + block.appendChild(node); + + // Add entry to stack for this opener + this.addBracket(node, startpos, false); + return true; +}; + +// IF next character is [, and ! delimiter to delimiter stack and +// add a text node to block's children. Otherwise just add a text node. +var parseBang = function(block) { + var startpos = this.pos; + this.pos += 1; + if (this.peek() === C_OPEN_BRACKET) { + this.pos += 1; + + var node = text('!['); + block.appendChild(node); + + // Add entry to stack for this opener + this.addBracket(node, startpos + 1, true); + } else { + block.appendChild(text('!')); + } + return true; +}; + +// Try to match close bracket against an opening in the delimiter +// stack. Add either a link or image, or a plain [ character, +// to block's children. If there is a matching delimiter, +// remove it from the delimiter stack. +var parseCloseBracket = function(block) { + var startpos; + var is_image; + var dest; + var title; + var matched = false; + var reflabel; + var opener; + + this.pos += 1; + startpos = this.pos; + + // get last [ or ![ + opener = this.brackets; + + if (opener === null) { + // no matched opener, just return a literal + block.appendChild(text(']')); + return true; + } + + if (!opener.active) { + // no matched opener, just return a literal + block.appendChild(text(']')); + // take opener off brackets stack + this.removeBracket(); + return true; + } + + // If we got here, open is a potential opener + is_image = opener.image; + + // Check to see if we have a link/image + + // Inline link? + if (this.peek() === C_OPEN_PAREN) { + this.pos++; + if (this.spnl() && + ((dest = this.parseLinkDestination()) !== null) && + this.spnl() && + // make sure there's a space before the title: + (reWhitespaceChar.test(this.subject.charAt(this.pos - 1)) && + (title = this.parseLinkTitle()) || true) && + this.spnl() && + this.peek() === C_CLOSE_PAREN) { + this.pos += 1; + matched = true; + } + } else { + + // Next, see if there's a link label + var savepos = this.pos; + var beforelabel = this.pos; + var n = this.parseLinkLabel(); + if (n > 2) { + reflabel = this.subject.slice(beforelabel, beforelabel + n); + } else if (!opener.bracketAfter) { + // Empty or missing second label means to use the first label as the reference. + // The reference must not contain a bracket. If we know there's a bracket, we don't even bother checking it. + reflabel = this.subject.slice(opener.index, startpos); + } + if (n === 0) { + // If shortcut reference link, rewind before spaces we skipped. + this.pos = savepos; + } + + if (reflabel) { + // lookup rawlabel in refmap + var link = this.refmap[normalizeReference(reflabel)]; + if (link) { + dest = link.destination; + title = link.title; + matched = true; + } + } + } + + if (matched) { + var node = new Node(is_image ? 'image' : 'link'); + node._destination = dest; + node._title = title || ''; + + var tmp, next; + tmp = opener.node._next; + while (tmp) { + next = tmp._next; + tmp.unlink(); + node.appendChild(tmp); + tmp = next; + } + block.appendChild(node); + this.processEmphasis(opener.previousDelimiter); + this.removeBracket(); + opener.node.unlink(); + + // We remove this bracket and processEmphasis will remove later delimiters. + // Now, for a link, we also deactivate earlier link openers. + // (no links in links) + if (!is_image) { + opener = this.brackets; + while (opener !== null) { + if (!opener.image) { + opener.active = false; // deactivate this opener + } + opener = opener.previous; + } + } + + return true; + + } else { // no match + + this.removeBracket(); // remove this opener from stack + this.pos = startpos; + block.appendChild(text(']')); + return true; + } + +}; + +var addBracket = function(node, index, image) { + if (this.brackets !== null) { + this.brackets.bracketAfter = true; + } + this.brackets = { node: node, + previous: this.brackets, + previousDelimiter: this.delimiters, + index: index, + image: image, + active: true }; +}; + +var removeBracket = function() { + this.brackets = this.brackets.previous; +}; + +// Attempt to parse an entity. +var parseEntity = function(block) { + var m; + if ((m = this.match(reEntityHere))) { + block.appendChild(text(decodeHTML(m))); + return true; + } else { + return false; + } +}; + +// Parse a run of ordinary characters, or a single character with +// a special meaning in markdown, as a plain string. +var parseString = function(block) { + var m; + if ((m = this.match(reMain))) { + if (this.options.smart) { + block.appendChild(text( + m.replace(reEllipses, "\u2026") + .replace(reDash, function(chars) { + var enCount = 0; + var emCount = 0; + if (chars.length % 3 === 0) { // If divisible by 3, use all em dashes + emCount = chars.length / 3; + } else if (chars.length % 2 === 0) { // If divisible by 2, use all en dashes + enCount = chars.length / 2; + } else if (chars.length % 3 === 2) { // If 2 extra dashes, use en dash for last 2; em dashes for rest + enCount = 1; + emCount = (chars.length - 2) / 3; + } else { // Use en dashes for last 4 hyphens; em dashes for rest + enCount = 2; + emCount = (chars.length - 4) / 3; + } + return "\u2014".repeat(emCount) + "\u2013".repeat(enCount); + }))); + } else { + block.appendChild(text(m)); + } + return true; + } else { + return false; + } +}; + +// Parse a newline. If it was preceded by two spaces, return a hard +// line break; otherwise a soft line break. +var parseNewline = function(block) { + this.pos += 1; // assume we're at a \n + // check previous node for trailing spaces + var lastc = block._lastChild; + if (lastc && lastc.type === 'text' && lastc._literal[lastc._literal.length - 1] === ' ') { + var hardbreak = lastc._literal[lastc._literal.length - 2] === ' '; + lastc._literal = lastc._literal.replace(reFinalSpace, ''); + block.appendChild(new Node(hardbreak ? 'linebreak' : 'softbreak')); + } else { + block.appendChild(new Node('softbreak')); + } + this.match(reInitialSpace); // gobble leading spaces in next line + return true; +}; + +// Attempt to parse a link reference, modifying refmap. +var parseReference = function(s, refmap) { + this.subject = s; + this.pos = 0; + var rawlabel; + var dest; + var title; + var matchChars; + var startpos = this.pos; + + // label: + matchChars = this.parseLinkLabel(); + if (matchChars === 0) { + return 0; + } else { + rawlabel = this.subject.substr(0, matchChars); + } + + // colon: + if (this.peek() === C_COLON) { + this.pos++; + } else { + this.pos = startpos; + return 0; + } + + // link url + this.spnl(); + + dest = this.parseLinkDestination(); + if (dest === null || dest.length === 0) { + this.pos = startpos; + return 0; + } + + var beforetitle = this.pos; + this.spnl(); + title = this.parseLinkTitle(); + if (title === null) { + title = ''; + // rewind before spaces + this.pos = beforetitle; + } + + // make sure we're at line end: + var atLineEnd = true; + if (this.match(reSpaceAtEndOfLine) === null) { + if (title === '') { + atLineEnd = false; + } else { + // the potential title we found is not at the line end, + // but it could still be a legal link reference if we + // discard the title + title = ''; + // rewind before spaces + this.pos = beforetitle; + // and instead check if the link URL is at the line end + atLineEnd = this.match(reSpaceAtEndOfLine) !== null; + } + } + + if (!atLineEnd) { + this.pos = startpos; + return 0; + } + + var normlabel = normalizeReference(rawlabel); + if (normlabel === '') { + // label must contain non-whitespace characters + this.pos = startpos; + return 0; + } + + if (!refmap[normlabel]) { + refmap[normlabel] = { destination: dest, title: title }; + } + return this.pos - startpos; +}; + +// Parse the next inline element in subject, advancing subject position. +// On success, add the result to block's children and return true. +// On failure, return false. +var parseInline = function(block) { + var res = false; + var c = this.peek(); + if (c === -1) { + return false; + } + switch(c) { + case C_NEWLINE: + res = this.parseNewline(block); + break; + case C_BACKSLASH: + res = this.parseBackslash(block); + break; + case C_BACKTICK: + res = this.parseBackticks(block); + break; + case C_ASTERISK: + case C_UNDERSCORE: + res = this.handleDelim(c, block); + break; + case C_SINGLEQUOTE: + case C_DOUBLEQUOTE: + res = this.options.smart && this.handleDelim(c, block); + break; + case C_OPEN_BRACKET: + res = this.parseOpenBracket(block); + break; + case C_BANG: + res = this.parseBang(block); + break; + case C_CLOSE_BRACKET: + res = this.parseCloseBracket(block); + break; + case C_LESSTHAN: + res = this.parseAutolink(block) || this.parseHtmlTag(block); + break; + case C_AMPERSAND: + res = this.parseEntity(block); + break; + default: + res = this.parseString(block); + break; + } + if (!res) { + this.pos += 1; + block.appendChild(text(fromCodePoint(c))); + } + + return true; +}; + +// Parse string content in block into inline children, +// using refmap to resolve references. +var parseInlines = function(block) { + this.subject = block._string_content.trim(); + this.pos = 0; + this.delimiters = null; + this.brackets = null; + while (this.parseInline(block)) { + } + block._string_content = null; // allow raw string to be garbage collected + this.processEmphasis(null); +}; + +// The InlineParser object. +function InlineParser(options){ + return { + subject: '', + delimiters: null, // used by handleDelim method + brackets: null, + pos: 0, + refmap: {}, + match: match, + peek: peek, + spnl: spnl, + parseBackticks: parseBackticks, + parseBackslash: parseBackslash, + parseAutolink: parseAutolink, + parseHtmlTag: parseHtmlTag, + scanDelims: scanDelims, + handleDelim: handleDelim, + parseLinkTitle: parseLinkTitle, + parseLinkDestination: parseLinkDestination, + parseLinkLabel: parseLinkLabel, + parseOpenBracket: parseOpenBracket, + parseBang: parseBang, + parseCloseBracket: parseCloseBracket, + addBracket: addBracket, + removeBracket: removeBracket, + parseEntity: parseEntity, + parseString: parseString, + parseNewline: parseNewline, + parseReference: parseReference, + parseInline: parseInline, + processEmphasis: processEmphasis, + removeDelimiter: removeDelimiter, + options: options || {}, + parse: parseInlines + }; +} + +module.exports = InlineParser; + +},{"./common":2,"./from-code-point.js":3,"./node":6,"./normalize-reference":7,"entities":11,"string.prototype.repeat":21}],6:[function(require,module,exports){ +"use strict"; + +function isContainer(node) { + switch (node._type) { + case 'document': + case 'block_quote': + case 'list': + case 'item': + case 'paragraph': + case 'heading': + case 'emph': + case 'strong': + case 'link': + case 'image': + case 'custom_inline': + case 'custom_block': + return true; + default: + return false; + } +} + +var resumeAt = function(node, entering) { + this.current = node; + this.entering = (entering === true); +}; + +var next = function(){ + var cur = this.current; + var entering = this.entering; + + if (cur === null) { + return null; + } + + var container = isContainer(cur); + + if (entering && container) { + if (cur._firstChild) { + this.current = cur._firstChild; + this.entering = true; + } else { + // stay on node but exit + this.entering = false; + } + + } else if (cur === this.root) { + this.current = null; + + } else if (cur._next === null) { + this.current = cur._parent; + this.entering = false; + + } else { + this.current = cur._next; + this.entering = true; + } + + return {entering: entering, node: cur}; +}; + +var NodeWalker = function(root) { + return { current: root, + root: root, + entering: true, + next: next, + resumeAt: resumeAt }; +}; + +var Node = function(nodeType, sourcepos) { + this._type = nodeType; + this._parent = null; + this._firstChild = null; + this._lastChild = null; + this._prev = null; + this._next = null; + this._sourcepos = sourcepos; + this._lastLineBlank = false; + this._open = true; + this._string_content = null; + this._literal = null; + this._listData = {}; + this._info = null; + this._destination = null; + this._title = null; + this._isFenced = false; + this._fenceChar = null; + this._fenceLength = 0; + this._fenceOffset = null; + this._level = null; + this._onEnter = null; + this._onExit = null; +}; + +var proto = Node.prototype; + +Object.defineProperty(proto, 'isContainer', { + get: function () { return isContainer(this); } +}); + +Object.defineProperty(proto, 'type', { + get: function() { return this._type; } +}); + +Object.defineProperty(proto, 'firstChild', { + get: function() { return this._firstChild; } +}); + +Object.defineProperty(proto, 'lastChild', { + get: function() { return this._lastChild; } +}); + +Object.defineProperty(proto, 'next', { + get: function() { return this._next; } +}); + +Object.defineProperty(proto, 'prev', { + get: function() { return this._prev; } +}); + +Object.defineProperty(proto, 'parent', { + get: function() { return this._parent; } +}); + +Object.defineProperty(proto, 'sourcepos', { + get: function() { return this._sourcepos; } +}); + +Object.defineProperty(proto, 'literal', { + get: function() { return this._literal; }, + set: function(s) { this._literal = s; } +}); + +Object.defineProperty(proto, 'destination', { + get: function() { return this._destination; }, + set: function(s) { this._destination = s; } +}); + +Object.defineProperty(proto, 'title', { + get: function() { return this._title; }, + set: function(s) { this._title = s; } +}); + +Object.defineProperty(proto, 'info', { + get: function() { return this._info; }, + set: function(s) { this._info = s; } +}); + +Object.defineProperty(proto, 'level', { + get: function() { return this._level; }, + set: function(s) { this._level = s; } +}); + +Object.defineProperty(proto, 'listType', { + get: function() { return this._listData.type; }, + set: function(t) { this._listData.type = t; } +}); + +Object.defineProperty(proto, 'listTight', { + get: function() { return this._listData.tight; }, + set: function(t) { this._listData.tight = t; } +}); + +Object.defineProperty(proto, 'listStart', { + get: function() { return this._listData.start; }, + set: function(n) { this._listData.start = n; } +}); + +Object.defineProperty(proto, 'listDelimiter', { + get: function() { return this._listData.delimiter; }, + set: function(delim) { this._listData.delimiter = delim; } +}); + +Object.defineProperty(proto, 'onEnter', { + get: function() { return this._onEnter; }, + set: function(s) { this._onEnter = s; } +}); + +Object.defineProperty(proto, 'onExit', { + get: function() { return this._onExit; }, + set: function(s) { this._onExit = s; } +}); + +Node.prototype.appendChild = function(child) { + child.unlink(); + child._parent = this; + if (this._lastChild) { + this._lastChild._next = child; + child._prev = this._lastChild; + this._lastChild = child; + } else { + this._firstChild = child; + this._lastChild = child; + } +}; + +Node.prototype.prependChild = function(child) { + child.unlink(); + child._parent = this; + if (this._firstChild) { + this._firstChild._prev = child; + child._next = this._firstChild; + this._firstChild = child; + } else { + this._firstChild = child; + this._lastChild = child; + } +}; + +Node.prototype.unlink = function() { + if (this._prev) { + this._prev._next = this._next; + } else if (this._parent) { + this._parent._firstChild = this._next; + } + if (this._next) { + this._next._prev = this._prev; + } else if (this._parent) { + this._parent._lastChild = this._prev; + } + this._parent = null; + this._next = null; + this._prev = null; +}; + +Node.prototype.insertAfter = function(sibling) { + sibling.unlink(); + sibling._next = this._next; + if (sibling._next) { + sibling._next._prev = sibling; + } + sibling._prev = this; + this._next = sibling; + sibling._parent = this._parent; + if (!sibling._next) { + sibling._parent._lastChild = sibling; + } +}; + +Node.prototype.insertBefore = function(sibling) { + sibling.unlink(); + sibling._prev = this._prev; + if (sibling._prev) { + sibling._prev._next = sibling; + } + sibling._next = this; + this._prev = sibling; + sibling._parent = this._parent; + if (!sibling._prev) { + sibling._parent._firstChild = sibling; + } +}; + +Node.prototype.walker = function() { + var walker = new NodeWalker(this); + return walker; +}; + +module.exports = Node; + + +/* Example of use of walker: + + var walker = w.walker(); + var event; + + while (event = walker.next()) { + console.log(event.entering, event.node.type); + } + + */ + +},{}],7:[function(require,module,exports){ +"use strict"; + +/* The bulk of this code derives from https://github.com/dmoscrop/fold-case +But in addition to case-folding, we also normalize whitespace. + +fold-case is Copyright Mathias Bynens + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/*eslint-disable key-spacing, comma-spacing */ + +var regex = /[ \t\r\n]+|[A-Z\xB5\xC0-\xD6\xD8-\xDF\u0100\u0102\u0104\u0106\u0108\u010A\u010C\u010E\u0110\u0112\u0114\u0116\u0118\u011A\u011C\u011E\u0120\u0122\u0124\u0126\u0128\u012A\u012C\u012E\u0130\u0132\u0134\u0136\u0139\u013B\u013D\u013F\u0141\u0143\u0145\u0147\u0149\u014A\u014C\u014E\u0150\u0152\u0154\u0156\u0158\u015A\u015C\u015E\u0160\u0162\u0164\u0166\u0168\u016A\u016C\u016E\u0170\u0172\u0174\u0176\u0178\u0179\u017B\u017D\u017F\u0181\u0182\u0184\u0186\u0187\u0189-\u018B\u018E-\u0191\u0193\u0194\u0196-\u0198\u019C\u019D\u019F\u01A0\u01A2\u01A4\u01A6\u01A7\u01A9\u01AC\u01AE\u01AF\u01B1-\u01B3\u01B5\u01B7\u01B8\u01BC\u01C4\u01C5\u01C7\u01C8\u01CA\u01CB\u01CD\u01CF\u01D1\u01D3\u01D5\u01D7\u01D9\u01DB\u01DE\u01E0\u01E2\u01E4\u01E6\u01E8\u01EA\u01EC\u01EE\u01F0-\u01F2\u01F4\u01F6-\u01F8\u01FA\u01FC\u01FE\u0200\u0202\u0204\u0206\u0208\u020A\u020C\u020E\u0210\u0212\u0214\u0216\u0218\u021A\u021C\u021E\u0220\u0222\u0224\u0226\u0228\u022A\u022C\u022E\u0230\u0232\u023A\u023B\u023D\u023E\u0241\u0243-\u0246\u0248\u024A\u024C\u024E\u0345\u0370\u0372\u0376\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03AB\u03B0\u03C2\u03CF-\u03D1\u03D5\u03D6\u03D8\u03DA\u03DC\u03DE\u03E0\u03E2\u03E4\u03E6\u03E8\u03EA\u03EC\u03EE\u03F0\u03F1\u03F4\u03F5\u03F7\u03F9\u03FA\u03FD-\u042F\u0460\u0462\u0464\u0466\u0468\u046A\u046C\u046E\u0470\u0472\u0474\u0476\u0478\u047A\u047C\u047E\u0480\u048A\u048C\u048E\u0490\u0492\u0494\u0496\u0498\u049A\u049C\u049E\u04A0\u04A2\u04A4\u04A6\u04A8\u04AA\u04AC\u04AE\u04B0\u04B2\u04B4\u04B6\u04B8\u04BA\u04BC\u04BE\u04C0\u04C1\u04C3\u04C5\u04C7\u04C9\u04CB\u04CD\u04D0\u04D2\u04D4\u04D6\u04D8\u04DA\u04DC\u04DE\u04E0\u04E2\u04E4\u04E6\u04E8\u04EA\u04EC\u04EE\u04F0\u04F2\u04F4\u04F6\u04F8\u04FA\u04FC\u04FE\u0500\u0502\u0504\u0506\u0508\u050A\u050C\u050E\u0510\u0512\u0514\u0516\u0518\u051A\u051C\u051E\u0520\u0522\u0524\u0526\u0528\u052A\u052C\u052E\u0531-\u0556\u0587\u10A0-\u10C5\u10C7\u10CD\u1E00\u1E02\u1E04\u1E06\u1E08\u1E0A\u1E0C\u1E0E\u1E10\u1E12\u1E14\u1E16\u1E18\u1E1A\u1E1C\u1E1E\u1E20\u1E22\u1E24\u1E26\u1E28\u1E2A\u1E2C\u1E2E\u1E30\u1E32\u1E34\u1E36\u1E38\u1E3A\u1E3C\u1E3E\u1E40\u1E42\u1E44\u1E46\u1E48\u1E4A\u1E4C\u1E4E\u1E50\u1E52\u1E54\u1E56\u1E58\u1E5A\u1E5C\u1E5E\u1E60\u1E62\u1E64\u1E66\u1E68\u1E6A\u1E6C\u1E6E\u1E70\u1E72\u1E74\u1E76\u1E78\u1E7A\u1E7C\u1E7E\u1E80\u1E82\u1E84\u1E86\u1E88\u1E8A\u1E8C\u1E8E\u1E90\u1E92\u1E94\u1E96-\u1E9B\u1E9E\u1EA0\u1EA2\u1EA4\u1EA6\u1EA8\u1EAA\u1EAC\u1EAE\u1EB0\u1EB2\u1EB4\u1EB6\u1EB8\u1EBA\u1EBC\u1EBE\u1EC0\u1EC2\u1EC4\u1EC6\u1EC8\u1ECA\u1ECC\u1ECE\u1ED0\u1ED2\u1ED4\u1ED6\u1ED8\u1EDA\u1EDC\u1EDE\u1EE0\u1EE2\u1EE4\u1EE6\u1EE8\u1EEA\u1EEC\u1EEE\u1EF0\u1EF2\u1EF4\u1EF6\u1EF8\u1EFA\u1EFC\u1EFE\u1F08-\u1F0F\u1F18-\u1F1D\u1F28-\u1F2F\u1F38-\u1F3F\u1F48-\u1F4D\u1F50\u1F52\u1F54\u1F56\u1F59\u1F5B\u1F5D\u1F5F\u1F68-\u1F6F\u1F80-\u1FAF\u1FB2-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD2\u1FD3\u1FD6-\u1FDB\u1FE2-\u1FE4\u1FE6-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2126\u212A\u212B\u2132\u2160-\u216F\u2183\u24B6-\u24CF\u2C00-\u2C2E\u2C60\u2C62-\u2C64\u2C67\u2C69\u2C6B\u2C6D-\u2C70\u2C72\u2C75\u2C7E-\u2C80\u2C82\u2C84\u2C86\u2C88\u2C8A\u2C8C\u2C8E\u2C90\u2C92\u2C94\u2C96\u2C98\u2C9A\u2C9C\u2C9E\u2CA0\u2CA2\u2CA4\u2CA6\u2CA8\u2CAA\u2CAC\u2CAE\u2CB0\u2CB2\u2CB4\u2CB6\u2CB8\u2CBA\u2CBC\u2CBE\u2CC0\u2CC2\u2CC4\u2CC6\u2CC8\u2CCA\u2CCC\u2CCE\u2CD0\u2CD2\u2CD4\u2CD6\u2CD8\u2CDA\u2CDC\u2CDE\u2CE0\u2CE2\u2CEB\u2CED\u2CF2\uA640\uA642\uA644\uA646\uA648\uA64A\uA64C\uA64E\uA650\uA652\uA654\uA656\uA658\uA65A\uA65C\uA65E\uA660\uA662\uA664\uA666\uA668\uA66A\uA66C\uA680\uA682\uA684\uA686\uA688\uA68A\uA68C\uA68E\uA690\uA692\uA694\uA696\uA698\uA69A\uA722\uA724\uA726\uA728\uA72A\uA72C\uA72E\uA732\uA734\uA736\uA738\uA73A\uA73C\uA73E\uA740\uA742\uA744\uA746\uA748\uA74A\uA74C\uA74E\uA750\uA752\uA754\uA756\uA758\uA75A\uA75C\uA75E\uA760\uA762\uA764\uA766\uA768\uA76A\uA76C\uA76E\uA779\uA77B\uA77D\uA77E\uA780\uA782\uA784\uA786\uA78B\uA78D\uA790\uA792\uA796\uA798\uA79A\uA79C\uA79E\uA7A0\uA7A2\uA7A4\uA7A6\uA7A8\uA7AA-\uA7AD\uA7B0\uA7B1\uFB00-\uFB06\uFB13-\uFB17\uFF21-\uFF3A]|\uD801[\uDC00-\uDC27]|\uD806[\uDCA0-\uDCBF]/g; + +var map = {'A':'a','B':'b','C':'c','D':'d','E':'e','F':'f','G':'g','H':'h','I':'i','J':'j','K':'k','L':'l','M':'m','N':'n','O':'o','P':'p','Q':'q','R':'r','S':'s','T':'t','U':'u','V':'v','W':'w','X':'x','Y':'y','Z':'z','\xB5':'\u03BC','\xC0':'\xE0','\xC1':'\xE1','\xC2':'\xE2','\xC3':'\xE3','\xC4':'\xE4','\xC5':'\xE5','\xC6':'\xE6','\xC7':'\xE7','\xC8':'\xE8','\xC9':'\xE9','\xCA':'\xEA','\xCB':'\xEB','\xCC':'\xEC','\xCD':'\xED','\xCE':'\xEE','\xCF':'\xEF','\xD0':'\xF0','\xD1':'\xF1','\xD2':'\xF2','\xD3':'\xF3','\xD4':'\xF4','\xD5':'\xF5','\xD6':'\xF6','\xD8':'\xF8','\xD9':'\xF9','\xDA':'\xFA','\xDB':'\xFB','\xDC':'\xFC','\xDD':'\xFD','\xDE':'\xFE','\u0100':'\u0101','\u0102':'\u0103','\u0104':'\u0105','\u0106':'\u0107','\u0108':'\u0109','\u010A':'\u010B','\u010C':'\u010D','\u010E':'\u010F','\u0110':'\u0111','\u0112':'\u0113','\u0114':'\u0115','\u0116':'\u0117','\u0118':'\u0119','\u011A':'\u011B','\u011C':'\u011D','\u011E':'\u011F','\u0120':'\u0121','\u0122':'\u0123','\u0124':'\u0125','\u0126':'\u0127','\u0128':'\u0129','\u012A':'\u012B','\u012C':'\u012D','\u012E':'\u012F','\u0132':'\u0133','\u0134':'\u0135','\u0136':'\u0137','\u0139':'\u013A','\u013B':'\u013C','\u013D':'\u013E','\u013F':'\u0140','\u0141':'\u0142','\u0143':'\u0144','\u0145':'\u0146','\u0147':'\u0148','\u014A':'\u014B','\u014C':'\u014D','\u014E':'\u014F','\u0150':'\u0151','\u0152':'\u0153','\u0154':'\u0155','\u0156':'\u0157','\u0158':'\u0159','\u015A':'\u015B','\u015C':'\u015D','\u015E':'\u015F','\u0160':'\u0161','\u0162':'\u0163','\u0164':'\u0165','\u0166':'\u0167','\u0168':'\u0169','\u016A':'\u016B','\u016C':'\u016D','\u016E':'\u016F','\u0170':'\u0171','\u0172':'\u0173','\u0174':'\u0175','\u0176':'\u0177','\u0178':'\xFF','\u0179':'\u017A','\u017B':'\u017C','\u017D':'\u017E','\u017F':'s','\u0181':'\u0253','\u0182':'\u0183','\u0184':'\u0185','\u0186':'\u0254','\u0187':'\u0188','\u0189':'\u0256','\u018A':'\u0257','\u018B':'\u018C','\u018E':'\u01DD','\u018F':'\u0259','\u0190':'\u025B','\u0191':'\u0192','\u0193':'\u0260','\u0194':'\u0263','\u0196':'\u0269','\u0197':'\u0268','\u0198':'\u0199','\u019C':'\u026F','\u019D':'\u0272','\u019F':'\u0275','\u01A0':'\u01A1','\u01A2':'\u01A3','\u01A4':'\u01A5','\u01A6':'\u0280','\u01A7':'\u01A8','\u01A9':'\u0283','\u01AC':'\u01AD','\u01AE':'\u0288','\u01AF':'\u01B0','\u01B1':'\u028A','\u01B2':'\u028B','\u01B3':'\u01B4','\u01B5':'\u01B6','\u01B7':'\u0292','\u01B8':'\u01B9','\u01BC':'\u01BD','\u01C4':'\u01C6','\u01C5':'\u01C6','\u01C7':'\u01C9','\u01C8':'\u01C9','\u01CA':'\u01CC','\u01CB':'\u01CC','\u01CD':'\u01CE','\u01CF':'\u01D0','\u01D1':'\u01D2','\u01D3':'\u01D4','\u01D5':'\u01D6','\u01D7':'\u01D8','\u01D9':'\u01DA','\u01DB':'\u01DC','\u01DE':'\u01DF','\u01E0':'\u01E1','\u01E2':'\u01E3','\u01E4':'\u01E5','\u01E6':'\u01E7','\u01E8':'\u01E9','\u01EA':'\u01EB','\u01EC':'\u01ED','\u01EE':'\u01EF','\u01F1':'\u01F3','\u01F2':'\u01F3','\u01F4':'\u01F5','\u01F6':'\u0195','\u01F7':'\u01BF','\u01F8':'\u01F9','\u01FA':'\u01FB','\u01FC':'\u01FD','\u01FE':'\u01FF','\u0200':'\u0201','\u0202':'\u0203','\u0204':'\u0205','\u0206':'\u0207','\u0208':'\u0209','\u020A':'\u020B','\u020C':'\u020D','\u020E':'\u020F','\u0210':'\u0211','\u0212':'\u0213','\u0214':'\u0215','\u0216':'\u0217','\u0218':'\u0219','\u021A':'\u021B','\u021C':'\u021D','\u021E':'\u021F','\u0220':'\u019E','\u0222':'\u0223','\u0224':'\u0225','\u0226':'\u0227','\u0228':'\u0229','\u022A':'\u022B','\u022C':'\u022D','\u022E':'\u022F','\u0230':'\u0231','\u0232':'\u0233','\u023A':'\u2C65','\u023B':'\u023C','\u023D':'\u019A','\u023E':'\u2C66','\u0241':'\u0242','\u0243':'\u0180','\u0244':'\u0289','\u0245':'\u028C','\u0246':'\u0247','\u0248':'\u0249','\u024A':'\u024B','\u024C':'\u024D','\u024E':'\u024F','\u0345':'\u03B9','\u0370':'\u0371','\u0372':'\u0373','\u0376':'\u0377','\u037F':'\u03F3','\u0386':'\u03AC','\u0388':'\u03AD','\u0389':'\u03AE','\u038A':'\u03AF','\u038C':'\u03CC','\u038E':'\u03CD','\u038F':'\u03CE','\u0391':'\u03B1','\u0392':'\u03B2','\u0393':'\u03B3','\u0394':'\u03B4','\u0395':'\u03B5','\u0396':'\u03B6','\u0397':'\u03B7','\u0398':'\u03B8','\u0399':'\u03B9','\u039A':'\u03BA','\u039B':'\u03BB','\u039C':'\u03BC','\u039D':'\u03BD','\u039E':'\u03BE','\u039F':'\u03BF','\u03A0':'\u03C0','\u03A1':'\u03C1','\u03A3':'\u03C3','\u03A4':'\u03C4','\u03A5':'\u03C5','\u03A6':'\u03C6','\u03A7':'\u03C7','\u03A8':'\u03C8','\u03A9':'\u03C9','\u03AA':'\u03CA','\u03AB':'\u03CB','\u03C2':'\u03C3','\u03CF':'\u03D7','\u03D0':'\u03B2','\u03D1':'\u03B8','\u03D5':'\u03C6','\u03D6':'\u03C0','\u03D8':'\u03D9','\u03DA':'\u03DB','\u03DC':'\u03DD','\u03DE':'\u03DF','\u03E0':'\u03E1','\u03E2':'\u03E3','\u03E4':'\u03E5','\u03E6':'\u03E7','\u03E8':'\u03E9','\u03EA':'\u03EB','\u03EC':'\u03ED','\u03EE':'\u03EF','\u03F0':'\u03BA','\u03F1':'\u03C1','\u03F4':'\u03B8','\u03F5':'\u03B5','\u03F7':'\u03F8','\u03F9':'\u03F2','\u03FA':'\u03FB','\u03FD':'\u037B','\u03FE':'\u037C','\u03FF':'\u037D','\u0400':'\u0450','\u0401':'\u0451','\u0402':'\u0452','\u0403':'\u0453','\u0404':'\u0454','\u0405':'\u0455','\u0406':'\u0456','\u0407':'\u0457','\u0408':'\u0458','\u0409':'\u0459','\u040A':'\u045A','\u040B':'\u045B','\u040C':'\u045C','\u040D':'\u045D','\u040E':'\u045E','\u040F':'\u045F','\u0410':'\u0430','\u0411':'\u0431','\u0412':'\u0432','\u0413':'\u0433','\u0414':'\u0434','\u0415':'\u0435','\u0416':'\u0436','\u0417':'\u0437','\u0418':'\u0438','\u0419':'\u0439','\u041A':'\u043A','\u041B':'\u043B','\u041C':'\u043C','\u041D':'\u043D','\u041E':'\u043E','\u041F':'\u043F','\u0420':'\u0440','\u0421':'\u0441','\u0422':'\u0442','\u0423':'\u0443','\u0424':'\u0444','\u0425':'\u0445','\u0426':'\u0446','\u0427':'\u0447','\u0428':'\u0448','\u0429':'\u0449','\u042A':'\u044A','\u042B':'\u044B','\u042C':'\u044C','\u042D':'\u044D','\u042E':'\u044E','\u042F':'\u044F','\u0460':'\u0461','\u0462':'\u0463','\u0464':'\u0465','\u0466':'\u0467','\u0468':'\u0469','\u046A':'\u046B','\u046C':'\u046D','\u046E':'\u046F','\u0470':'\u0471','\u0472':'\u0473','\u0474':'\u0475','\u0476':'\u0477','\u0478':'\u0479','\u047A':'\u047B','\u047C':'\u047D','\u047E':'\u047F','\u0480':'\u0481','\u048A':'\u048B','\u048C':'\u048D','\u048E':'\u048F','\u0490':'\u0491','\u0492':'\u0493','\u0494':'\u0495','\u0496':'\u0497','\u0498':'\u0499','\u049A':'\u049B','\u049C':'\u049D','\u049E':'\u049F','\u04A0':'\u04A1','\u04A2':'\u04A3','\u04A4':'\u04A5','\u04A6':'\u04A7','\u04A8':'\u04A9','\u04AA':'\u04AB','\u04AC':'\u04AD','\u04AE':'\u04AF','\u04B0':'\u04B1','\u04B2':'\u04B3','\u04B4':'\u04B5','\u04B6':'\u04B7','\u04B8':'\u04B9','\u04BA':'\u04BB','\u04BC':'\u04BD','\u04BE':'\u04BF','\u04C0':'\u04CF','\u04C1':'\u04C2','\u04C3':'\u04C4','\u04C5':'\u04C6','\u04C7':'\u04C8','\u04C9':'\u04CA','\u04CB':'\u04CC','\u04CD':'\u04CE','\u04D0':'\u04D1','\u04D2':'\u04D3','\u04D4':'\u04D5','\u04D6':'\u04D7','\u04D8':'\u04D9','\u04DA':'\u04DB','\u04DC':'\u04DD','\u04DE':'\u04DF','\u04E0':'\u04E1','\u04E2':'\u04E3','\u04E4':'\u04E5','\u04E6':'\u04E7','\u04E8':'\u04E9','\u04EA':'\u04EB','\u04EC':'\u04ED','\u04EE':'\u04EF','\u04F0':'\u04F1','\u04F2':'\u04F3','\u04F4':'\u04F5','\u04F6':'\u04F7','\u04F8':'\u04F9','\u04FA':'\u04FB','\u04FC':'\u04FD','\u04FE':'\u04FF','\u0500':'\u0501','\u0502':'\u0503','\u0504':'\u0505','\u0506':'\u0507','\u0508':'\u0509','\u050A':'\u050B','\u050C':'\u050D','\u050E':'\u050F','\u0510':'\u0511','\u0512':'\u0513','\u0514':'\u0515','\u0516':'\u0517','\u0518':'\u0519','\u051A':'\u051B','\u051C':'\u051D','\u051E':'\u051F','\u0520':'\u0521','\u0522':'\u0523','\u0524':'\u0525','\u0526':'\u0527','\u0528':'\u0529','\u052A':'\u052B','\u052C':'\u052D','\u052E':'\u052F','\u0531':'\u0561','\u0532':'\u0562','\u0533':'\u0563','\u0534':'\u0564','\u0535':'\u0565','\u0536':'\u0566','\u0537':'\u0567','\u0538':'\u0568','\u0539':'\u0569','\u053A':'\u056A','\u053B':'\u056B','\u053C':'\u056C','\u053D':'\u056D','\u053E':'\u056E','\u053F':'\u056F','\u0540':'\u0570','\u0541':'\u0571','\u0542':'\u0572','\u0543':'\u0573','\u0544':'\u0574','\u0545':'\u0575','\u0546':'\u0576','\u0547':'\u0577','\u0548':'\u0578','\u0549':'\u0579','\u054A':'\u057A','\u054B':'\u057B','\u054C':'\u057C','\u054D':'\u057D','\u054E':'\u057E','\u054F':'\u057F','\u0550':'\u0580','\u0551':'\u0581','\u0552':'\u0582','\u0553':'\u0583','\u0554':'\u0584','\u0555':'\u0585','\u0556':'\u0586','\u10A0':'\u2D00','\u10A1':'\u2D01','\u10A2':'\u2D02','\u10A3':'\u2D03','\u10A4':'\u2D04','\u10A5':'\u2D05','\u10A6':'\u2D06','\u10A7':'\u2D07','\u10A8':'\u2D08','\u10A9':'\u2D09','\u10AA':'\u2D0A','\u10AB':'\u2D0B','\u10AC':'\u2D0C','\u10AD':'\u2D0D','\u10AE':'\u2D0E','\u10AF':'\u2D0F','\u10B0':'\u2D10','\u10B1':'\u2D11','\u10B2':'\u2D12','\u10B3':'\u2D13','\u10B4':'\u2D14','\u10B5':'\u2D15','\u10B6':'\u2D16','\u10B7':'\u2D17','\u10B8':'\u2D18','\u10B9':'\u2D19','\u10BA':'\u2D1A','\u10BB':'\u2D1B','\u10BC':'\u2D1C','\u10BD':'\u2D1D','\u10BE':'\u2D1E','\u10BF':'\u2D1F','\u10C0':'\u2D20','\u10C1':'\u2D21','\u10C2':'\u2D22','\u10C3':'\u2D23','\u10C4':'\u2D24','\u10C5':'\u2D25','\u10C7':'\u2D27','\u10CD':'\u2D2D','\u1E00':'\u1E01','\u1E02':'\u1E03','\u1E04':'\u1E05','\u1E06':'\u1E07','\u1E08':'\u1E09','\u1E0A':'\u1E0B','\u1E0C':'\u1E0D','\u1E0E':'\u1E0F','\u1E10':'\u1E11','\u1E12':'\u1E13','\u1E14':'\u1E15','\u1E16':'\u1E17','\u1E18':'\u1E19','\u1E1A':'\u1E1B','\u1E1C':'\u1E1D','\u1E1E':'\u1E1F','\u1E20':'\u1E21','\u1E22':'\u1E23','\u1E24':'\u1E25','\u1E26':'\u1E27','\u1E28':'\u1E29','\u1E2A':'\u1E2B','\u1E2C':'\u1E2D','\u1E2E':'\u1E2F','\u1E30':'\u1E31','\u1E32':'\u1E33','\u1E34':'\u1E35','\u1E36':'\u1E37','\u1E38':'\u1E39','\u1E3A':'\u1E3B','\u1E3C':'\u1E3D','\u1E3E':'\u1E3F','\u1E40':'\u1E41','\u1E42':'\u1E43','\u1E44':'\u1E45','\u1E46':'\u1E47','\u1E48':'\u1E49','\u1E4A':'\u1E4B','\u1E4C':'\u1E4D','\u1E4E':'\u1E4F','\u1E50':'\u1E51','\u1E52':'\u1E53','\u1E54':'\u1E55','\u1E56':'\u1E57','\u1E58':'\u1E59','\u1E5A':'\u1E5B','\u1E5C':'\u1E5D','\u1E5E':'\u1E5F','\u1E60':'\u1E61','\u1E62':'\u1E63','\u1E64':'\u1E65','\u1E66':'\u1E67','\u1E68':'\u1E69','\u1E6A':'\u1E6B','\u1E6C':'\u1E6D','\u1E6E':'\u1E6F','\u1E70':'\u1E71','\u1E72':'\u1E73','\u1E74':'\u1E75','\u1E76':'\u1E77','\u1E78':'\u1E79','\u1E7A':'\u1E7B','\u1E7C':'\u1E7D','\u1E7E':'\u1E7F','\u1E80':'\u1E81','\u1E82':'\u1E83','\u1E84':'\u1E85','\u1E86':'\u1E87','\u1E88':'\u1E89','\u1E8A':'\u1E8B','\u1E8C':'\u1E8D','\u1E8E':'\u1E8F','\u1E90':'\u1E91','\u1E92':'\u1E93','\u1E94':'\u1E95','\u1E9B':'\u1E61','\u1EA0':'\u1EA1','\u1EA2':'\u1EA3','\u1EA4':'\u1EA5','\u1EA6':'\u1EA7','\u1EA8':'\u1EA9','\u1EAA':'\u1EAB','\u1EAC':'\u1EAD','\u1EAE':'\u1EAF','\u1EB0':'\u1EB1','\u1EB2':'\u1EB3','\u1EB4':'\u1EB5','\u1EB6':'\u1EB7','\u1EB8':'\u1EB9','\u1EBA':'\u1EBB','\u1EBC':'\u1EBD','\u1EBE':'\u1EBF','\u1EC0':'\u1EC1','\u1EC2':'\u1EC3','\u1EC4':'\u1EC5','\u1EC6':'\u1EC7','\u1EC8':'\u1EC9','\u1ECA':'\u1ECB','\u1ECC':'\u1ECD','\u1ECE':'\u1ECF','\u1ED0':'\u1ED1','\u1ED2':'\u1ED3','\u1ED4':'\u1ED5','\u1ED6':'\u1ED7','\u1ED8':'\u1ED9','\u1EDA':'\u1EDB','\u1EDC':'\u1EDD','\u1EDE':'\u1EDF','\u1EE0':'\u1EE1','\u1EE2':'\u1EE3','\u1EE4':'\u1EE5','\u1EE6':'\u1EE7','\u1EE8':'\u1EE9','\u1EEA':'\u1EEB','\u1EEC':'\u1EED','\u1EEE':'\u1EEF','\u1EF0':'\u1EF1','\u1EF2':'\u1EF3','\u1EF4':'\u1EF5','\u1EF6':'\u1EF7','\u1EF8':'\u1EF9','\u1EFA':'\u1EFB','\u1EFC':'\u1EFD','\u1EFE':'\u1EFF','\u1F08':'\u1F00','\u1F09':'\u1F01','\u1F0A':'\u1F02','\u1F0B':'\u1F03','\u1F0C':'\u1F04','\u1F0D':'\u1F05','\u1F0E':'\u1F06','\u1F0F':'\u1F07','\u1F18':'\u1F10','\u1F19':'\u1F11','\u1F1A':'\u1F12','\u1F1B':'\u1F13','\u1F1C':'\u1F14','\u1F1D':'\u1F15','\u1F28':'\u1F20','\u1F29':'\u1F21','\u1F2A':'\u1F22','\u1F2B':'\u1F23','\u1F2C':'\u1F24','\u1F2D':'\u1F25','\u1F2E':'\u1F26','\u1F2F':'\u1F27','\u1F38':'\u1F30','\u1F39':'\u1F31','\u1F3A':'\u1F32','\u1F3B':'\u1F33','\u1F3C':'\u1F34','\u1F3D':'\u1F35','\u1F3E':'\u1F36','\u1F3F':'\u1F37','\u1F48':'\u1F40','\u1F49':'\u1F41','\u1F4A':'\u1F42','\u1F4B':'\u1F43','\u1F4C':'\u1F44','\u1F4D':'\u1F45','\u1F59':'\u1F51','\u1F5B':'\u1F53','\u1F5D':'\u1F55','\u1F5F':'\u1F57','\u1F68':'\u1F60','\u1F69':'\u1F61','\u1F6A':'\u1F62','\u1F6B':'\u1F63','\u1F6C':'\u1F64','\u1F6D':'\u1F65','\u1F6E':'\u1F66','\u1F6F':'\u1F67','\u1FB8':'\u1FB0','\u1FB9':'\u1FB1','\u1FBA':'\u1F70','\u1FBB':'\u1F71','\u1FBE':'\u03B9','\u1FC8':'\u1F72','\u1FC9':'\u1F73','\u1FCA':'\u1F74','\u1FCB':'\u1F75','\u1FD8':'\u1FD0','\u1FD9':'\u1FD1','\u1FDA':'\u1F76','\u1FDB':'\u1F77','\u1FE8':'\u1FE0','\u1FE9':'\u1FE1','\u1FEA':'\u1F7A','\u1FEB':'\u1F7B','\u1FEC':'\u1FE5','\u1FF8':'\u1F78','\u1FF9':'\u1F79','\u1FFA':'\u1F7C','\u1FFB':'\u1F7D','\u2126':'\u03C9','\u212A':'k','\u212B':'\xE5','\u2132':'\u214E','\u2160':'\u2170','\u2161':'\u2171','\u2162':'\u2172','\u2163':'\u2173','\u2164':'\u2174','\u2165':'\u2175','\u2166':'\u2176','\u2167':'\u2177','\u2168':'\u2178','\u2169':'\u2179','\u216A':'\u217A','\u216B':'\u217B','\u216C':'\u217C','\u216D':'\u217D','\u216E':'\u217E','\u216F':'\u217F','\u2183':'\u2184','\u24B6':'\u24D0','\u24B7':'\u24D1','\u24B8':'\u24D2','\u24B9':'\u24D3','\u24BA':'\u24D4','\u24BB':'\u24D5','\u24BC':'\u24D6','\u24BD':'\u24D7','\u24BE':'\u24D8','\u24BF':'\u24D9','\u24C0':'\u24DA','\u24C1':'\u24DB','\u24C2':'\u24DC','\u24C3':'\u24DD','\u24C4':'\u24DE','\u24C5':'\u24DF','\u24C6':'\u24E0','\u24C7':'\u24E1','\u24C8':'\u24E2','\u24C9':'\u24E3','\u24CA':'\u24E4','\u24CB':'\u24E5','\u24CC':'\u24E6','\u24CD':'\u24E7','\u24CE':'\u24E8','\u24CF':'\u24E9','\u2C00':'\u2C30','\u2C01':'\u2C31','\u2C02':'\u2C32','\u2C03':'\u2C33','\u2C04':'\u2C34','\u2C05':'\u2C35','\u2C06':'\u2C36','\u2C07':'\u2C37','\u2C08':'\u2C38','\u2C09':'\u2C39','\u2C0A':'\u2C3A','\u2C0B':'\u2C3B','\u2C0C':'\u2C3C','\u2C0D':'\u2C3D','\u2C0E':'\u2C3E','\u2C0F':'\u2C3F','\u2C10':'\u2C40','\u2C11':'\u2C41','\u2C12':'\u2C42','\u2C13':'\u2C43','\u2C14':'\u2C44','\u2C15':'\u2C45','\u2C16':'\u2C46','\u2C17':'\u2C47','\u2C18':'\u2C48','\u2C19':'\u2C49','\u2C1A':'\u2C4A','\u2C1B':'\u2C4B','\u2C1C':'\u2C4C','\u2C1D':'\u2C4D','\u2C1E':'\u2C4E','\u2C1F':'\u2C4F','\u2C20':'\u2C50','\u2C21':'\u2C51','\u2C22':'\u2C52','\u2C23':'\u2C53','\u2C24':'\u2C54','\u2C25':'\u2C55','\u2C26':'\u2C56','\u2C27':'\u2C57','\u2C28':'\u2C58','\u2C29':'\u2C59','\u2C2A':'\u2C5A','\u2C2B':'\u2C5B','\u2C2C':'\u2C5C','\u2C2D':'\u2C5D','\u2C2E':'\u2C5E','\u2C60':'\u2C61','\u2C62':'\u026B','\u2C63':'\u1D7D','\u2C64':'\u027D','\u2C67':'\u2C68','\u2C69':'\u2C6A','\u2C6B':'\u2C6C','\u2C6D':'\u0251','\u2C6E':'\u0271','\u2C6F':'\u0250','\u2C70':'\u0252','\u2C72':'\u2C73','\u2C75':'\u2C76','\u2C7E':'\u023F','\u2C7F':'\u0240','\u2C80':'\u2C81','\u2C82':'\u2C83','\u2C84':'\u2C85','\u2C86':'\u2C87','\u2C88':'\u2C89','\u2C8A':'\u2C8B','\u2C8C':'\u2C8D','\u2C8E':'\u2C8F','\u2C90':'\u2C91','\u2C92':'\u2C93','\u2C94':'\u2C95','\u2C96':'\u2C97','\u2C98':'\u2C99','\u2C9A':'\u2C9B','\u2C9C':'\u2C9D','\u2C9E':'\u2C9F','\u2CA0':'\u2CA1','\u2CA2':'\u2CA3','\u2CA4':'\u2CA5','\u2CA6':'\u2CA7','\u2CA8':'\u2CA9','\u2CAA':'\u2CAB','\u2CAC':'\u2CAD','\u2CAE':'\u2CAF','\u2CB0':'\u2CB1','\u2CB2':'\u2CB3','\u2CB4':'\u2CB5','\u2CB6':'\u2CB7','\u2CB8':'\u2CB9','\u2CBA':'\u2CBB','\u2CBC':'\u2CBD','\u2CBE':'\u2CBF','\u2CC0':'\u2CC1','\u2CC2':'\u2CC3','\u2CC4':'\u2CC5','\u2CC6':'\u2CC7','\u2CC8':'\u2CC9','\u2CCA':'\u2CCB','\u2CCC':'\u2CCD','\u2CCE':'\u2CCF','\u2CD0':'\u2CD1','\u2CD2':'\u2CD3','\u2CD4':'\u2CD5','\u2CD6':'\u2CD7','\u2CD8':'\u2CD9','\u2CDA':'\u2CDB','\u2CDC':'\u2CDD','\u2CDE':'\u2CDF','\u2CE0':'\u2CE1','\u2CE2':'\u2CE3','\u2CEB':'\u2CEC','\u2CED':'\u2CEE','\u2CF2':'\u2CF3','\uA640':'\uA641','\uA642':'\uA643','\uA644':'\uA645','\uA646':'\uA647','\uA648':'\uA649','\uA64A':'\uA64B','\uA64C':'\uA64D','\uA64E':'\uA64F','\uA650':'\uA651','\uA652':'\uA653','\uA654':'\uA655','\uA656':'\uA657','\uA658':'\uA659','\uA65A':'\uA65B','\uA65C':'\uA65D','\uA65E':'\uA65F','\uA660':'\uA661','\uA662':'\uA663','\uA664':'\uA665','\uA666':'\uA667','\uA668':'\uA669','\uA66A':'\uA66B','\uA66C':'\uA66D','\uA680':'\uA681','\uA682':'\uA683','\uA684':'\uA685','\uA686':'\uA687','\uA688':'\uA689','\uA68A':'\uA68B','\uA68C':'\uA68D','\uA68E':'\uA68F','\uA690':'\uA691','\uA692':'\uA693','\uA694':'\uA695','\uA696':'\uA697','\uA698':'\uA699','\uA69A':'\uA69B','\uA722':'\uA723','\uA724':'\uA725','\uA726':'\uA727','\uA728':'\uA729','\uA72A':'\uA72B','\uA72C':'\uA72D','\uA72E':'\uA72F','\uA732':'\uA733','\uA734':'\uA735','\uA736':'\uA737','\uA738':'\uA739','\uA73A':'\uA73B','\uA73C':'\uA73D','\uA73E':'\uA73F','\uA740':'\uA741','\uA742':'\uA743','\uA744':'\uA745','\uA746':'\uA747','\uA748':'\uA749','\uA74A':'\uA74B','\uA74C':'\uA74D','\uA74E':'\uA74F','\uA750':'\uA751','\uA752':'\uA753','\uA754':'\uA755','\uA756':'\uA757','\uA758':'\uA759','\uA75A':'\uA75B','\uA75C':'\uA75D','\uA75E':'\uA75F','\uA760':'\uA761','\uA762':'\uA763','\uA764':'\uA765','\uA766':'\uA767','\uA768':'\uA769','\uA76A':'\uA76B','\uA76C':'\uA76D','\uA76E':'\uA76F','\uA779':'\uA77A','\uA77B':'\uA77C','\uA77D':'\u1D79','\uA77E':'\uA77F','\uA780':'\uA781','\uA782':'\uA783','\uA784':'\uA785','\uA786':'\uA787','\uA78B':'\uA78C','\uA78D':'\u0265','\uA790':'\uA791','\uA792':'\uA793','\uA796':'\uA797','\uA798':'\uA799','\uA79A':'\uA79B','\uA79C':'\uA79D','\uA79E':'\uA79F','\uA7A0':'\uA7A1','\uA7A2':'\uA7A3','\uA7A4':'\uA7A5','\uA7A6':'\uA7A7','\uA7A8':'\uA7A9','\uA7AA':'\u0266','\uA7AB':'\u025C','\uA7AC':'\u0261','\uA7AD':'\u026C','\uA7B0':'\u029E','\uA7B1':'\u0287','\uFF21':'\uFF41','\uFF22':'\uFF42','\uFF23':'\uFF43','\uFF24':'\uFF44','\uFF25':'\uFF45','\uFF26':'\uFF46','\uFF27':'\uFF47','\uFF28':'\uFF48','\uFF29':'\uFF49','\uFF2A':'\uFF4A','\uFF2B':'\uFF4B','\uFF2C':'\uFF4C','\uFF2D':'\uFF4D','\uFF2E':'\uFF4E','\uFF2F':'\uFF4F','\uFF30':'\uFF50','\uFF31':'\uFF51','\uFF32':'\uFF52','\uFF33':'\uFF53','\uFF34':'\uFF54','\uFF35':'\uFF55','\uFF36':'\uFF56','\uFF37':'\uFF57','\uFF38':'\uFF58','\uFF39':'\uFF59','\uFF3A':'\uFF5A','\uD801\uDC00':'\uD801\uDC28','\uD801\uDC01':'\uD801\uDC29','\uD801\uDC02':'\uD801\uDC2A','\uD801\uDC03':'\uD801\uDC2B','\uD801\uDC04':'\uD801\uDC2C','\uD801\uDC05':'\uD801\uDC2D','\uD801\uDC06':'\uD801\uDC2E','\uD801\uDC07':'\uD801\uDC2F','\uD801\uDC08':'\uD801\uDC30','\uD801\uDC09':'\uD801\uDC31','\uD801\uDC0A':'\uD801\uDC32','\uD801\uDC0B':'\uD801\uDC33','\uD801\uDC0C':'\uD801\uDC34','\uD801\uDC0D':'\uD801\uDC35','\uD801\uDC0E':'\uD801\uDC36','\uD801\uDC0F':'\uD801\uDC37','\uD801\uDC10':'\uD801\uDC38','\uD801\uDC11':'\uD801\uDC39','\uD801\uDC12':'\uD801\uDC3A','\uD801\uDC13':'\uD801\uDC3B','\uD801\uDC14':'\uD801\uDC3C','\uD801\uDC15':'\uD801\uDC3D','\uD801\uDC16':'\uD801\uDC3E','\uD801\uDC17':'\uD801\uDC3F','\uD801\uDC18':'\uD801\uDC40','\uD801\uDC19':'\uD801\uDC41','\uD801\uDC1A':'\uD801\uDC42','\uD801\uDC1B':'\uD801\uDC43','\uD801\uDC1C':'\uD801\uDC44','\uD801\uDC1D':'\uD801\uDC45','\uD801\uDC1E':'\uD801\uDC46','\uD801\uDC1F':'\uD801\uDC47','\uD801\uDC20':'\uD801\uDC48','\uD801\uDC21':'\uD801\uDC49','\uD801\uDC22':'\uD801\uDC4A','\uD801\uDC23':'\uD801\uDC4B','\uD801\uDC24':'\uD801\uDC4C','\uD801\uDC25':'\uD801\uDC4D','\uD801\uDC26':'\uD801\uDC4E','\uD801\uDC27':'\uD801\uDC4F','\uD806\uDCA0':'\uD806\uDCC0','\uD806\uDCA1':'\uD806\uDCC1','\uD806\uDCA2':'\uD806\uDCC2','\uD806\uDCA3':'\uD806\uDCC3','\uD806\uDCA4':'\uD806\uDCC4','\uD806\uDCA5':'\uD806\uDCC5','\uD806\uDCA6':'\uD806\uDCC6','\uD806\uDCA7':'\uD806\uDCC7','\uD806\uDCA8':'\uD806\uDCC8','\uD806\uDCA9':'\uD806\uDCC9','\uD806\uDCAA':'\uD806\uDCCA','\uD806\uDCAB':'\uD806\uDCCB','\uD806\uDCAC':'\uD806\uDCCC','\uD806\uDCAD':'\uD806\uDCCD','\uD806\uDCAE':'\uD806\uDCCE','\uD806\uDCAF':'\uD806\uDCCF','\uD806\uDCB0':'\uD806\uDCD0','\uD806\uDCB1':'\uD806\uDCD1','\uD806\uDCB2':'\uD806\uDCD2','\uD806\uDCB3':'\uD806\uDCD3','\uD806\uDCB4':'\uD806\uDCD4','\uD806\uDCB5':'\uD806\uDCD5','\uD806\uDCB6':'\uD806\uDCD6','\uD806\uDCB7':'\uD806\uDCD7','\uD806\uDCB8':'\uD806\uDCD8','\uD806\uDCB9':'\uD806\uDCD9','\uD806\uDCBA':'\uD806\uDCDA','\uD806\uDCBB':'\uD806\uDCDB','\uD806\uDCBC':'\uD806\uDCDC','\uD806\uDCBD':'\uD806\uDCDD','\uD806\uDCBE':'\uD806\uDCDE','\uD806\uDCBF':'\uD806\uDCDF','\xDF':'ss','\u0130':'i\u0307','\u0149':'\u02BCn','\u01F0':'j\u030C','\u0390':'\u03B9\u0308\u0301','\u03B0':'\u03C5\u0308\u0301','\u0587':'\u0565\u0582','\u1E96':'h\u0331','\u1E97':'t\u0308','\u1E98':'w\u030A','\u1E99':'y\u030A','\u1E9A':'a\u02BE','\u1E9E':'ss','\u1F50':'\u03C5\u0313','\u1F52':'\u03C5\u0313\u0300','\u1F54':'\u03C5\u0313\u0301','\u1F56':'\u03C5\u0313\u0342','\u1F80':'\u1F00\u03B9','\u1F81':'\u1F01\u03B9','\u1F82':'\u1F02\u03B9','\u1F83':'\u1F03\u03B9','\u1F84':'\u1F04\u03B9','\u1F85':'\u1F05\u03B9','\u1F86':'\u1F06\u03B9','\u1F87':'\u1F07\u03B9','\u1F88':'\u1F00\u03B9','\u1F89':'\u1F01\u03B9','\u1F8A':'\u1F02\u03B9','\u1F8B':'\u1F03\u03B9','\u1F8C':'\u1F04\u03B9','\u1F8D':'\u1F05\u03B9','\u1F8E':'\u1F06\u03B9','\u1F8F':'\u1F07\u03B9','\u1F90':'\u1F20\u03B9','\u1F91':'\u1F21\u03B9','\u1F92':'\u1F22\u03B9','\u1F93':'\u1F23\u03B9','\u1F94':'\u1F24\u03B9','\u1F95':'\u1F25\u03B9','\u1F96':'\u1F26\u03B9','\u1F97':'\u1F27\u03B9','\u1F98':'\u1F20\u03B9','\u1F99':'\u1F21\u03B9','\u1F9A':'\u1F22\u03B9','\u1F9B':'\u1F23\u03B9','\u1F9C':'\u1F24\u03B9','\u1F9D':'\u1F25\u03B9','\u1F9E':'\u1F26\u03B9','\u1F9F':'\u1F27\u03B9','\u1FA0':'\u1F60\u03B9','\u1FA1':'\u1F61\u03B9','\u1FA2':'\u1F62\u03B9','\u1FA3':'\u1F63\u03B9','\u1FA4':'\u1F64\u03B9','\u1FA5':'\u1F65\u03B9','\u1FA6':'\u1F66\u03B9','\u1FA7':'\u1F67\u03B9','\u1FA8':'\u1F60\u03B9','\u1FA9':'\u1F61\u03B9','\u1FAA':'\u1F62\u03B9','\u1FAB':'\u1F63\u03B9','\u1FAC':'\u1F64\u03B9','\u1FAD':'\u1F65\u03B9','\u1FAE':'\u1F66\u03B9','\u1FAF':'\u1F67\u03B9','\u1FB2':'\u1F70\u03B9','\u1FB3':'\u03B1\u03B9','\u1FB4':'\u03AC\u03B9','\u1FB6':'\u03B1\u0342','\u1FB7':'\u03B1\u0342\u03B9','\u1FBC':'\u03B1\u03B9','\u1FC2':'\u1F74\u03B9','\u1FC3':'\u03B7\u03B9','\u1FC4':'\u03AE\u03B9','\u1FC6':'\u03B7\u0342','\u1FC7':'\u03B7\u0342\u03B9','\u1FCC':'\u03B7\u03B9','\u1FD2':'\u03B9\u0308\u0300','\u1FD3':'\u03B9\u0308\u0301','\u1FD6':'\u03B9\u0342','\u1FD7':'\u03B9\u0308\u0342','\u1FE2':'\u03C5\u0308\u0300','\u1FE3':'\u03C5\u0308\u0301','\u1FE4':'\u03C1\u0313','\u1FE6':'\u03C5\u0342','\u1FE7':'\u03C5\u0308\u0342','\u1FF2':'\u1F7C\u03B9','\u1FF3':'\u03C9\u03B9','\u1FF4':'\u03CE\u03B9','\u1FF6':'\u03C9\u0342','\u1FF7':'\u03C9\u0342\u03B9','\u1FFC':'\u03C9\u03B9','\uFB00':'ff','\uFB01':'fi','\uFB02':'fl','\uFB03':'ffi','\uFB04':'ffl','\uFB05':'st','\uFB06':'st','\uFB13':'\u0574\u0576','\uFB14':'\u0574\u0565','\uFB15':'\u0574\u056B','\uFB16':'\u057E\u0576','\uFB17':'\u0574\u056D'}; + +// Normalize reference label: collapse internal whitespace +// to single space, remove leading/trailing whitespace, case fold. +module.exports = function(string) { + return string.slice(1, string.length - 1).trim().replace(regex, function($0) { + // Note: there is no need to check `hasOwnProperty($0)` here. + // If character not found in lookup table, it must be whitespace. + return map[$0] || ' '; + }); +}; + +},{}],8:[function(require,module,exports){ +"use strict"; + +var Renderer = require('./renderer'); + +var esc = require('../common').escapeXml; + +var reUnsafeProtocol = /^javascript:|vbscript:|file:|data:/i; +var reSafeDataProtocol = /^data:image\/(?:png|gif|jpeg|webp)/i; + +var potentiallyUnsafe = function(url) { + return reUnsafeProtocol.test(url) && + !reSafeDataProtocol.test(url); +}; + +// Helper function to produce an HTML tag. +function tag(name, attrs, selfclosing) { + if (this.disableTags > 0) { + return; + } + this.buffer += ('<' + name); + if (attrs && attrs.length > 0) { + var i = 0; + var attrib; + while ((attrib = attrs[i]) !== undefined) { + this.buffer += (' ' + attrib[0] + '="' + attrib[1] + '"'); + i++; + } + } + if (selfclosing) { + this.buffer += ' /'; + } + this.buffer += '>'; + this.lastOut = '>'; +} + + +function HtmlRenderer(options) { + options = options || {}; + // by default, soft breaks are rendered as newlines in HTML + options.softbreak = options.softbreak || '\n'; + // set to "
" to make them hard breaks + // set to " " if you want to ignore line wrapping in source + + this.disableTags = 0; + this.lastOut = "\n"; + this.options = options; +} + +/* Node methods */ + +function text(node) { + this.out(node.literal); +} + +function softbreak() { + this.lit(this.options.softbreak); +} + +function linebreak() { + this.tag('br', [], true); + this.cr(); +} + +function link(node, entering) { + var attrs = this.attrs(node); + if (entering) { + if (!(this.options.safe && potentiallyUnsafe(node.destination))) { + attrs.push(['href', esc(node.destination, true)]); + } + if (node.title) { + attrs.push(['title', esc(node.title, true)]); + } + this.tag('a', attrs); + } else { + this.tag('/a'); + } +} + +function image(node, entering) { + if (entering) { + if (this.disableTags === 0) { + if (this.options.safe && + potentiallyUnsafe(node.destination)) { + this.lit('');
+          } else {
+              this.lit('<img src='); + } + } +} + +function emph(node, entering) { + this.tag(entering ? 'em' : '/em'); +} + +function strong(node, entering) { + this.tag(entering ? 'strong' : '/strong'); +} + +function paragraph(node, entering) { + var grandparent = node.parent.parent + , attrs = this.attrs(node); + if (grandparent !== null && + grandparent.type === 'list') { + if (grandparent.listTight) { + return; + } + } + if (entering) { + this.cr(); + this.tag('p', attrs); + } else { + this.tag('/p'); + this.cr(); + } +} + +function heading(node, entering) { + var tagname = 'h' + node.level + , attrs = this.attrs(node); + if (entering) { + this.cr(); + this.tag(tagname, attrs); + } else { + this.tag('/' + tagname); + this.cr(); + } +} + +function code(node) { + this.tag('code'); + this.out(node.literal); + this.tag('/code'); +} + +function code_block(node) { + var info_words = node.info ? node.info.split(/\s+/) : [] + , attrs = this.attrs(node); + if (info_words.length > 0 && info_words[0].length > 0) { + attrs.push(['class', 'language-' + esc(info_words[0], true)]); + } + this.cr(); + this.tag('pre'); + this.tag('code', attrs); + this.out(node.literal); + this.tag('/code'); + this.tag('/pre'); + this.cr(); +} + +function thematic_break(node) { + var attrs = this.attrs(node); + this.cr(); + this.tag('hr', attrs, true); + this.cr(); +} + +function block_quote(node, entering) { + var attrs = this.attrs(node); + if (entering) { + this.cr(); + this.tag('blockquote', attrs); + this.cr(); + } else { + this.cr(); + this.tag('/blockquote'); + this.cr(); + } +} + +function list(node, entering) { + var tagname = node.listType === 'bullet' ? 'ul' : 'ol' + , attrs = this.attrs(node); + + if (entering) { + var start = node.listStart; + if (start !== null && start !== 1) { + attrs.push(['start', start.toString()]); + } + this.cr(); + this.tag(tagname, attrs); + this.cr(); + } else { + this.cr(); + this.tag('/' + tagname); + this.cr(); + } +} + +function item(node, entering) { + var attrs = this.attrs(node); + if (entering) { + this.tag('li', attrs); + } else { + this.tag('/li'); + this.cr(); + } +} + +function html_inline(node) { + if (this.options.safe) { + this.lit(''); + } else { + this.lit(node.literal); + } +} + +function html_block(node) { + this.cr(); + if (this.options.safe) { + this.lit(''); + } else { + this.lit(node.literal); + } + this.cr(); +} + +function custom_inline(node, entering) { + if (entering && node.onEnter) { + this.lit(node.onEnter); + } else if (!entering && node.onExit) { + this.lit(node.onExit); + } +} + +function custom_block(node, entering) { + this.cr(); + if (entering && node.onEnter) { + this.lit(node.onEnter); + } else if (!entering && node.onExit) { + this.lit(node.onExit); + } + this.cr(); +} + +/* Helper methods */ + +function out(s) { + this.lit(esc(s, false)); +} + +function attrs (node) { + var att = []; + if (this.options.sourcepos) { + var pos = node.sourcepos; + if (pos) { + att.push(['data-sourcepos', String(pos[0][0]) + ':' + + String(pos[0][1]) + '-' + String(pos[1][0]) + ':' + + String(pos[1][1])]); + } + } + return att; +} + +// quick browser-compatible inheritance +HtmlRenderer.prototype = Object.create(Renderer.prototype); + +HtmlRenderer.prototype.text = text; +HtmlRenderer.prototype.html_inline = html_inline; +HtmlRenderer.prototype.html_block = html_block; +HtmlRenderer.prototype.softbreak = softbreak; +HtmlRenderer.prototype.linebreak = linebreak; +HtmlRenderer.prototype.link = link; +HtmlRenderer.prototype.image = image; +HtmlRenderer.prototype.emph = emph; +HtmlRenderer.prototype.strong = strong; +HtmlRenderer.prototype.paragraph = paragraph; +HtmlRenderer.prototype.heading = heading; +HtmlRenderer.prototype.code = code; +HtmlRenderer.prototype.code_block = code_block; +HtmlRenderer.prototype.thematic_break = thematic_break; +HtmlRenderer.prototype.block_quote = block_quote; +HtmlRenderer.prototype.list = list; +HtmlRenderer.prototype.item = item; +HtmlRenderer.prototype.custom_inline = custom_inline; +HtmlRenderer.prototype.custom_block = custom_block; + +HtmlRenderer.prototype.out = out; +HtmlRenderer.prototype.tag = tag; +HtmlRenderer.prototype.attrs = attrs; + +module.exports = HtmlRenderer; + +},{"../common":2,"./renderer":9}],9:[function(require,module,exports){ +"use strict"; + +function Renderer() {} + +/** + * Walks the AST and calls member methods for each Node type. + * + * @param ast {Node} The root of the abstract syntax tree. + */ +function render(ast) { + var walker = ast.walker() + , event + , type; + + this.buffer = ''; + this.lastOut = '\n'; + + while((event = walker.next())) { + type = event.node.type; + if (this[type]) { + this[type](event.node, event.entering); + } + } + return this.buffer; +} + +/** + * Concatenate a literal string to the buffer. + * + * @param str {String} The string to concatenate. + */ +function lit(str) { + this.buffer += str; + this.lastOut = str; +} + +function cr() { + if (this.lastOut !== '\n') { + this.lit('\n'); + } +} + +/** + * Concatenate a string to the buffer possibly escaping the content. + * + * Concrete renderer implementations should override this method. + * + * @param str {String} The string to concatenate. + */ +function out(str) { + this.lit(str); +} + +Renderer.prototype.render = render; +Renderer.prototype.out = out; +Renderer.prototype.lit = lit; +Renderer.prototype.cr = cr; + +module.exports = Renderer; + +},{}],10:[function(require,module,exports){ +"use strict"; + +var escapeXml = require('./common').escapeXml; + +// Helper function to produce an XML tag. +var tag = function(name, attrs, selfclosing) { + var result = '<' + name; + if (attrs && attrs.length > 0) { + var i = 0; + var attrib; + while ((attrib = attrs[i]) !== undefined) { + result += ' ' + attrib[0] + '="' + escapeXml(attrib[1]) + '"'; + i++; + } + } + if (selfclosing) { + result += ' /'; + } + + result += '>'; + return result; +}; + +var reXMLTag = /\<[^>]*\>/; + +var toTagName = function(s) { + return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase(); +}; + +var renderNodes = function(block) { + + var attrs; + var tagname; + var walker = block.walker(); + var event, node, entering; + var buffer = ""; + var lastOut = "\n"; + var disableTags = 0; + var indentLevel = 0; + var indent = ' '; + var container; + var selfClosing; + var nodetype; + + var out = function(s) { + if (disableTags > 0) { + buffer += s.replace(reXMLTag, ''); + } else { + buffer += s; + } + lastOut = s; + }; + var esc = this.escape; + var cr = function() { + if (lastOut !== '\n') { + buffer += '\n'; + lastOut = '\n'; + for (var i = indentLevel; i > 0; i--) { + buffer += indent; + } + } + }; + + var options = this.options; + + if (options.time) { console.time("rendering"); } + + buffer += '\n'; + buffer += '\n'; + + while ((event = walker.next())) { + entering = event.entering; + node = event.node; + nodetype = node.type; + + container = node.isContainer; + selfClosing = nodetype === 'thematic_break' || nodetype === 'linebreak' || + nodetype === 'softbreak'; + tagname = toTagName(nodetype); + + if (entering) { + + attrs = []; + + switch (nodetype) { + case 'document': + attrs.push(['xmlns', 'http://commonmark.org/xml/1.0']); + break; + case 'list': + if (node.listType !== null) { + attrs.push(['type', node.listType.toLowerCase()]); + } + if (node.listStart !== null) { + attrs.push(['start', String(node.listStart)]); + } + if (node.listTight !== null) { + attrs.push(['tight', (node.listTight ? 'true' : 'false')]); + } + var delim = node.listDelimiter; + if (delim !== null) { + var delimword = ''; + if (delim === '.') { + delimword = 'period'; + } else { + delimword = 'paren'; + } + attrs.push(['delimiter', delimword]); + } + break; + case 'code_block': + if (node.info) { + attrs.push(['info', node.info]); + } + break; + case 'heading': + attrs.push(['level', String(node.level)]); + break; + case 'link': + case 'image': + attrs.push(['destination', node.destination]); + attrs.push(['title', node.title]); + break; + case 'custom_inline': + case 'custom_block': + attrs.push(['on_enter', node.onEnter]); + attrs.push(['on_exit', node.onExit]); + break; + default: + break; + } + if (options.sourcepos) { + var pos = node.sourcepos; + if (pos) { + attrs.push(['sourcepos', String(pos[0][0]) + ':' + + String(pos[0][1]) + '-' + String(pos[1][0]) + ':' + + String(pos[1][1])]); + } + } + + cr(); + out(tag(tagname, attrs, selfClosing)); + if (container) { + indentLevel += 1; + } else if (!container && !selfClosing) { + var lit = node.literal; + if (lit) { + out(esc(lit)); + } + out(tag('/' + tagname)); + } + } else { + indentLevel -= 1; + cr(); + out(tag('/' + tagname)); + } + + + } + if (options.time) { console.timeEnd("rendering"); } + buffer += '\n'; + return buffer; +}; + +// The XmlRenderer object. +function XmlRenderer(options){ + return { + // default options: + softbreak: '\n', // by default, soft breaks are rendered as newlines in HTML + // set to "
" to make them hard breaks + // set to " " if you want to ignore line wrapping in source + escape: escapeXml, + options: options || {}, + render: renderNodes + }; +} + +module.exports = XmlRenderer; + +},{"./common":2}],11:[function(require,module,exports){ +var encode = require("./lib/encode.js"), + decode = require("./lib/decode.js"); + +exports.decode = function(data, level){ + return (!level || level <= 0 ? decode.XML : decode.HTML)(data); +}; + +exports.decodeStrict = function(data, level){ + return (!level || level <= 0 ? decode.XML : decode.HTMLStrict)(data); +}; + +exports.encode = function(data, level){ + return (!level || level <= 0 ? encode.XML : encode.HTML)(data); +}; + +exports.encodeXML = encode.XML; + +exports.encodeHTML4 = +exports.encodeHTML5 = +exports.encodeHTML = encode.HTML; + +exports.decodeXML = +exports.decodeXMLStrict = decode.XML; + +exports.decodeHTML4 = +exports.decodeHTML5 = +exports.decodeHTML = decode.HTML; + +exports.decodeHTML4Strict = +exports.decodeHTML5Strict = +exports.decodeHTMLStrict = decode.HTMLStrict; + +exports.escape = encode.escape; + +},{"./lib/decode.js":12,"./lib/encode.js":14}],12:[function(require,module,exports){ +var entityMap = require("../maps/entities.json"), + legacyMap = require("../maps/legacy.json"), + xmlMap = require("../maps/xml.json"), + decodeCodePoint = require("./decode_codepoint.js"); + +var decodeXMLStrict = getStrictDecoder(xmlMap), + decodeHTMLStrict = getStrictDecoder(entityMap); + +function getStrictDecoder(map){ + var keys = Object.keys(map).join("|"), + replace = getReplacer(map); + + keys += "|#[xX][\\da-fA-F]+|#\\d+"; + + var re = new RegExp("&(?:" + keys + ");", "g"); + + return function(str){ + return String(str).replace(re, replace); + }; +} + +var decodeHTML = (function(){ + var legacy = Object.keys(legacyMap) + .sort(sorter); + + var keys = Object.keys(entityMap) + .sort(sorter); + + for(var i = 0, j = 0; i < keys.length; i++){ + if(legacy[j] === keys[i]){ + keys[i] += ";?"; + j++; + } else { + keys[i] += ";"; + } + } + + var re = new RegExp("&(?:" + keys.join("|") + "|#[xX][\\da-fA-F]+;?|#\\d+;?)", "g"), + replace = getReplacer(entityMap); + + function replacer(str){ + if(str.substr(-1) !== ";") str += ";"; + return replace(str); + } + + //TODO consider creating a merged map + return function(str){ + return String(str).replace(re, replacer); + }; +}()); + +function sorter(a, b){ + return a < b ? 1 : -1; +} + +function getReplacer(map){ + return function replace(str){ + if(str.charAt(1) === "#"){ + if(str.charAt(2) === "X" || str.charAt(2) === "x"){ + return decodeCodePoint(parseInt(str.substr(3), 16)); + } + return decodeCodePoint(parseInt(str.substr(2), 10)); + } + return map[str.slice(1, -1)]; + }; +} + +module.exports = { + XML: decodeXMLStrict, + HTML: decodeHTML, + HTMLStrict: decodeHTMLStrict +}; +},{"../maps/entities.json":16,"../maps/legacy.json":17,"../maps/xml.json":18,"./decode_codepoint.js":13}],13:[function(require,module,exports){ +var decodeMap = require("../maps/decode.json"); + +module.exports = decodeCodePoint; + +// modified version of https://github.com/mathiasbynens/he/blob/master/src/he.js#L94-L119 +function decodeCodePoint(codePoint){ + + if((codePoint >= 0xD800 && codePoint <= 0xDFFF) || codePoint > 0x10FFFF){ + return "\uFFFD"; + } + + if(codePoint in decodeMap){ + codePoint = decodeMap[codePoint]; + } + + var output = ""; + + if(codePoint > 0xFFFF){ + codePoint -= 0x10000; + output += String.fromCharCode(codePoint >>> 10 & 0x3FF | 0xD800); + codePoint = 0xDC00 | codePoint & 0x3FF; + } + + output += String.fromCharCode(codePoint); + return output; +} + +},{"../maps/decode.json":15}],14:[function(require,module,exports){ +var inverseXML = getInverseObj(require("../maps/xml.json")), + xmlReplacer = getInverseReplacer(inverseXML); + +exports.XML = getInverse(inverseXML, xmlReplacer); + +var inverseHTML = getInverseObj(require("../maps/entities.json")), + htmlReplacer = getInverseReplacer(inverseHTML); + +exports.HTML = getInverse(inverseHTML, htmlReplacer); + +function getInverseObj(obj){ + return Object.keys(obj).sort().reduce(function(inverse, name){ + inverse[obj[name]] = "&" + name + ";"; + return inverse; + }, {}); +} + +function getInverseReplacer(inverse){ + var single = [], + multiple = []; + + Object.keys(inverse).forEach(function(k){ + if(k.length === 1){ + single.push("\\" + k); + } else { + multiple.push(k); + } + }); + + //TODO add ranges + multiple.unshift("[" + single.join("") + "]"); + + return new RegExp(multiple.join("|"), "g"); +} + +var re_nonASCII = /[^\0-\x7F]/g, + re_astralSymbols = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g; + +function singleCharReplacer(c){ + return "&#x" + c.charCodeAt(0).toString(16).toUpperCase() + ";"; +} + +function astralReplacer(c){ + // http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae + var high = c.charCodeAt(0); + var low = c.charCodeAt(1); + var codePoint = (high - 0xD800) * 0x400 + low - 0xDC00 + 0x10000; + return "&#x" + codePoint.toString(16).toUpperCase() + ";"; +} + +function getInverse(inverse, re){ + function func(name){ + return inverse[name]; + } + + return function(data){ + return data + .replace(re, func) + .replace(re_astralSymbols, astralReplacer) + .replace(re_nonASCII, singleCharReplacer); + }; +} + +var re_xmlChars = getInverseReplacer(inverseXML); + +function escapeXML(data){ + return data + .replace(re_xmlChars, singleCharReplacer) + .replace(re_astralSymbols, astralReplacer) + .replace(re_nonASCII, singleCharReplacer); +} + +exports.escape = escapeXML; + +},{"../maps/entities.json":16,"../maps/xml.json":18}],15:[function(require,module,exports){ +module.exports={"0":65533,"128":8364,"130":8218,"131":402,"132":8222,"133":8230,"134":8224,"135":8225,"136":710,"137":8240,"138":352,"139":8249,"140":338,"142":381,"145":8216,"146":8217,"147":8220,"148":8221,"149":8226,"150":8211,"151":8212,"152":732,"153":8482,"154":353,"155":8250,"156":339,"158":382,"159":376} +},{}],16:[function(require,module,exports){ +module.exports={"Aacute":"\u00C1","aacute":"\u00E1","Abreve":"\u0102","abreve":"\u0103","ac":"\u223E","acd":"\u223F","acE":"\u223E\u0333","Acirc":"\u00C2","acirc":"\u00E2","acute":"\u00B4","Acy":"\u0410","acy":"\u0430","AElig":"\u00C6","aelig":"\u00E6","af":"\u2061","Afr":"\uD835\uDD04","afr":"\uD835\uDD1E","Agrave":"\u00C0","agrave":"\u00E0","alefsym":"\u2135","aleph":"\u2135","Alpha":"\u0391","alpha":"\u03B1","Amacr":"\u0100","amacr":"\u0101","amalg":"\u2A3F","amp":"&","AMP":"&","andand":"\u2A55","And":"\u2A53","and":"\u2227","andd":"\u2A5C","andslope":"\u2A58","andv":"\u2A5A","ang":"\u2220","ange":"\u29A4","angle":"\u2220","angmsdaa":"\u29A8","angmsdab":"\u29A9","angmsdac":"\u29AA","angmsdad":"\u29AB","angmsdae":"\u29AC","angmsdaf":"\u29AD","angmsdag":"\u29AE","angmsdah":"\u29AF","angmsd":"\u2221","angrt":"\u221F","angrtvb":"\u22BE","angrtvbd":"\u299D","angsph":"\u2222","angst":"\u00C5","angzarr":"\u237C","Aogon":"\u0104","aogon":"\u0105","Aopf":"\uD835\uDD38","aopf":"\uD835\uDD52","apacir":"\u2A6F","ap":"\u2248","apE":"\u2A70","ape":"\u224A","apid":"\u224B","apos":"'","ApplyFunction":"\u2061","approx":"\u2248","approxeq":"\u224A","Aring":"\u00C5","aring":"\u00E5","Ascr":"\uD835\uDC9C","ascr":"\uD835\uDCB6","Assign":"\u2254","ast":"*","asymp":"\u2248","asympeq":"\u224D","Atilde":"\u00C3","atilde":"\u00E3","Auml":"\u00C4","auml":"\u00E4","awconint":"\u2233","awint":"\u2A11","backcong":"\u224C","backepsilon":"\u03F6","backprime":"\u2035","backsim":"\u223D","backsimeq":"\u22CD","Backslash":"\u2216","Barv":"\u2AE7","barvee":"\u22BD","barwed":"\u2305","Barwed":"\u2306","barwedge":"\u2305","bbrk":"\u23B5","bbrktbrk":"\u23B6","bcong":"\u224C","Bcy":"\u0411","bcy":"\u0431","bdquo":"\u201E","becaus":"\u2235","because":"\u2235","Because":"\u2235","bemptyv":"\u29B0","bepsi":"\u03F6","bernou":"\u212C","Bernoullis":"\u212C","Beta":"\u0392","beta":"\u03B2","beth":"\u2136","between":"\u226C","Bfr":"\uD835\uDD05","bfr":"\uD835\uDD1F","bigcap":"\u22C2","bigcirc":"\u25EF","bigcup":"\u22C3","bigodot":"\u2A00","bigoplus":"\u2A01","bigotimes":"\u2A02","bigsqcup":"\u2A06","bigstar":"\u2605","bigtriangledown":"\u25BD","bigtriangleup":"\u25B3","biguplus":"\u2A04","bigvee":"\u22C1","bigwedge":"\u22C0","bkarow":"\u290D","blacklozenge":"\u29EB","blacksquare":"\u25AA","blacktriangle":"\u25B4","blacktriangledown":"\u25BE","blacktriangleleft":"\u25C2","blacktriangleright":"\u25B8","blank":"\u2423","blk12":"\u2592","blk14":"\u2591","blk34":"\u2593","block":"\u2588","bne":"=\u20E5","bnequiv":"\u2261\u20E5","bNot":"\u2AED","bnot":"\u2310","Bopf":"\uD835\uDD39","bopf":"\uD835\uDD53","bot":"\u22A5","bottom":"\u22A5","bowtie":"\u22C8","boxbox":"\u29C9","boxdl":"\u2510","boxdL":"\u2555","boxDl":"\u2556","boxDL":"\u2557","boxdr":"\u250C","boxdR":"\u2552","boxDr":"\u2553","boxDR":"\u2554","boxh":"\u2500","boxH":"\u2550","boxhd":"\u252C","boxHd":"\u2564","boxhD":"\u2565","boxHD":"\u2566","boxhu":"\u2534","boxHu":"\u2567","boxhU":"\u2568","boxHU":"\u2569","boxminus":"\u229F","boxplus":"\u229E","boxtimes":"\u22A0","boxul":"\u2518","boxuL":"\u255B","boxUl":"\u255C","boxUL":"\u255D","boxur":"\u2514","boxuR":"\u2558","boxUr":"\u2559","boxUR":"\u255A","boxv":"\u2502","boxV":"\u2551","boxvh":"\u253C","boxvH":"\u256A","boxVh":"\u256B","boxVH":"\u256C","boxvl":"\u2524","boxvL":"\u2561","boxVl":"\u2562","boxVL":"\u2563","boxvr":"\u251C","boxvR":"\u255E","boxVr":"\u255F","boxVR":"\u2560","bprime":"\u2035","breve":"\u02D8","Breve":"\u02D8","brvbar":"\u00A6","bscr":"\uD835\uDCB7","Bscr":"\u212C","bsemi":"\u204F","bsim":"\u223D","bsime":"\u22CD","bsolb":"\u29C5","bsol":"\\","bsolhsub":"\u27C8","bull":"\u2022","bullet":"\u2022","bump":"\u224E","bumpE":"\u2AAE","bumpe":"\u224F","Bumpeq":"\u224E","bumpeq":"\u224F","Cacute":"\u0106","cacute":"\u0107","capand":"\u2A44","capbrcup":"\u2A49","capcap":"\u2A4B","cap":"\u2229","Cap":"\u22D2","capcup":"\u2A47","capdot":"\u2A40","CapitalDifferentialD":"\u2145","caps":"\u2229\uFE00","caret":"\u2041","caron":"\u02C7","Cayleys":"\u212D","ccaps":"\u2A4D","Ccaron":"\u010C","ccaron":"\u010D","Ccedil":"\u00C7","ccedil":"\u00E7","Ccirc":"\u0108","ccirc":"\u0109","Cconint":"\u2230","ccups":"\u2A4C","ccupssm":"\u2A50","Cdot":"\u010A","cdot":"\u010B","cedil":"\u00B8","Cedilla":"\u00B8","cemptyv":"\u29B2","cent":"\u00A2","centerdot":"\u00B7","CenterDot":"\u00B7","cfr":"\uD835\uDD20","Cfr":"\u212D","CHcy":"\u0427","chcy":"\u0447","check":"\u2713","checkmark":"\u2713","Chi":"\u03A7","chi":"\u03C7","circ":"\u02C6","circeq":"\u2257","circlearrowleft":"\u21BA","circlearrowright":"\u21BB","circledast":"\u229B","circledcirc":"\u229A","circleddash":"\u229D","CircleDot":"\u2299","circledR":"\u00AE","circledS":"\u24C8","CircleMinus":"\u2296","CirclePlus":"\u2295","CircleTimes":"\u2297","cir":"\u25CB","cirE":"\u29C3","cire":"\u2257","cirfnint":"\u2A10","cirmid":"\u2AEF","cirscir":"\u29C2","ClockwiseContourIntegral":"\u2232","CloseCurlyDoubleQuote":"\u201D","CloseCurlyQuote":"\u2019","clubs":"\u2663","clubsuit":"\u2663","colon":":","Colon":"\u2237","Colone":"\u2A74","colone":"\u2254","coloneq":"\u2254","comma":",","commat":"@","comp":"\u2201","compfn":"\u2218","complement":"\u2201","complexes":"\u2102","cong":"\u2245","congdot":"\u2A6D","Congruent":"\u2261","conint":"\u222E","Conint":"\u222F","ContourIntegral":"\u222E","copf":"\uD835\uDD54","Copf":"\u2102","coprod":"\u2210","Coproduct":"\u2210","copy":"\u00A9","COPY":"\u00A9","copysr":"\u2117","CounterClockwiseContourIntegral":"\u2233","crarr":"\u21B5","cross":"\u2717","Cross":"\u2A2F","Cscr":"\uD835\uDC9E","cscr":"\uD835\uDCB8","csub":"\u2ACF","csube":"\u2AD1","csup":"\u2AD0","csupe":"\u2AD2","ctdot":"\u22EF","cudarrl":"\u2938","cudarrr":"\u2935","cuepr":"\u22DE","cuesc":"\u22DF","cularr":"\u21B6","cularrp":"\u293D","cupbrcap":"\u2A48","cupcap":"\u2A46","CupCap":"\u224D","cup":"\u222A","Cup":"\u22D3","cupcup":"\u2A4A","cupdot":"\u228D","cupor":"\u2A45","cups":"\u222A\uFE00","curarr":"\u21B7","curarrm":"\u293C","curlyeqprec":"\u22DE","curlyeqsucc":"\u22DF","curlyvee":"\u22CE","curlywedge":"\u22CF","curren":"\u00A4","curvearrowleft":"\u21B6","curvearrowright":"\u21B7","cuvee":"\u22CE","cuwed":"\u22CF","cwconint":"\u2232","cwint":"\u2231","cylcty":"\u232D","dagger":"\u2020","Dagger":"\u2021","daleth":"\u2138","darr":"\u2193","Darr":"\u21A1","dArr":"\u21D3","dash":"\u2010","Dashv":"\u2AE4","dashv":"\u22A3","dbkarow":"\u290F","dblac":"\u02DD","Dcaron":"\u010E","dcaron":"\u010F","Dcy":"\u0414","dcy":"\u0434","ddagger":"\u2021","ddarr":"\u21CA","DD":"\u2145","dd":"\u2146","DDotrahd":"\u2911","ddotseq":"\u2A77","deg":"\u00B0","Del":"\u2207","Delta":"\u0394","delta":"\u03B4","demptyv":"\u29B1","dfisht":"\u297F","Dfr":"\uD835\uDD07","dfr":"\uD835\uDD21","dHar":"\u2965","dharl":"\u21C3","dharr":"\u21C2","DiacriticalAcute":"\u00B4","DiacriticalDot":"\u02D9","DiacriticalDoubleAcute":"\u02DD","DiacriticalGrave":"`","DiacriticalTilde":"\u02DC","diam":"\u22C4","diamond":"\u22C4","Diamond":"\u22C4","diamondsuit":"\u2666","diams":"\u2666","die":"\u00A8","DifferentialD":"\u2146","digamma":"\u03DD","disin":"\u22F2","div":"\u00F7","divide":"\u00F7","divideontimes":"\u22C7","divonx":"\u22C7","DJcy":"\u0402","djcy":"\u0452","dlcorn":"\u231E","dlcrop":"\u230D","dollar":"$","Dopf":"\uD835\uDD3B","dopf":"\uD835\uDD55","Dot":"\u00A8","dot":"\u02D9","DotDot":"\u20DC","doteq":"\u2250","doteqdot":"\u2251","DotEqual":"\u2250","dotminus":"\u2238","dotplus":"\u2214","dotsquare":"\u22A1","doublebarwedge":"\u2306","DoubleContourIntegral":"\u222F","DoubleDot":"\u00A8","DoubleDownArrow":"\u21D3","DoubleLeftArrow":"\u21D0","DoubleLeftRightArrow":"\u21D4","DoubleLeftTee":"\u2AE4","DoubleLongLeftArrow":"\u27F8","DoubleLongLeftRightArrow":"\u27FA","DoubleLongRightArrow":"\u27F9","DoubleRightArrow":"\u21D2","DoubleRightTee":"\u22A8","DoubleUpArrow":"\u21D1","DoubleUpDownArrow":"\u21D5","DoubleVerticalBar":"\u2225","DownArrowBar":"\u2913","downarrow":"\u2193","DownArrow":"\u2193","Downarrow":"\u21D3","DownArrowUpArrow":"\u21F5","DownBreve":"\u0311","downdownarrows":"\u21CA","downharpoonleft":"\u21C3","downharpoonright":"\u21C2","DownLeftRightVector":"\u2950","DownLeftTeeVector":"\u295E","DownLeftVectorBar":"\u2956","DownLeftVector":"\u21BD","DownRightTeeVector":"\u295F","DownRightVectorBar":"\u2957","DownRightVector":"\u21C1","DownTeeArrow":"\u21A7","DownTee":"\u22A4","drbkarow":"\u2910","drcorn":"\u231F","drcrop":"\u230C","Dscr":"\uD835\uDC9F","dscr":"\uD835\uDCB9","DScy":"\u0405","dscy":"\u0455","dsol":"\u29F6","Dstrok":"\u0110","dstrok":"\u0111","dtdot":"\u22F1","dtri":"\u25BF","dtrif":"\u25BE","duarr":"\u21F5","duhar":"\u296F","dwangle":"\u29A6","DZcy":"\u040F","dzcy":"\u045F","dzigrarr":"\u27FF","Eacute":"\u00C9","eacute":"\u00E9","easter":"\u2A6E","Ecaron":"\u011A","ecaron":"\u011B","Ecirc":"\u00CA","ecirc":"\u00EA","ecir":"\u2256","ecolon":"\u2255","Ecy":"\u042D","ecy":"\u044D","eDDot":"\u2A77","Edot":"\u0116","edot":"\u0117","eDot":"\u2251","ee":"\u2147","efDot":"\u2252","Efr":"\uD835\uDD08","efr":"\uD835\uDD22","eg":"\u2A9A","Egrave":"\u00C8","egrave":"\u00E8","egs":"\u2A96","egsdot":"\u2A98","el":"\u2A99","Element":"\u2208","elinters":"\u23E7","ell":"\u2113","els":"\u2A95","elsdot":"\u2A97","Emacr":"\u0112","emacr":"\u0113","empty":"\u2205","emptyset":"\u2205","EmptySmallSquare":"\u25FB","emptyv":"\u2205","EmptyVerySmallSquare":"\u25AB","emsp13":"\u2004","emsp14":"\u2005","emsp":"\u2003","ENG":"\u014A","eng":"\u014B","ensp":"\u2002","Eogon":"\u0118","eogon":"\u0119","Eopf":"\uD835\uDD3C","eopf":"\uD835\uDD56","epar":"\u22D5","eparsl":"\u29E3","eplus":"\u2A71","epsi":"\u03B5","Epsilon":"\u0395","epsilon":"\u03B5","epsiv":"\u03F5","eqcirc":"\u2256","eqcolon":"\u2255","eqsim":"\u2242","eqslantgtr":"\u2A96","eqslantless":"\u2A95","Equal":"\u2A75","equals":"=","EqualTilde":"\u2242","equest":"\u225F","Equilibrium":"\u21CC","equiv":"\u2261","equivDD":"\u2A78","eqvparsl":"\u29E5","erarr":"\u2971","erDot":"\u2253","escr":"\u212F","Escr":"\u2130","esdot":"\u2250","Esim":"\u2A73","esim":"\u2242","Eta":"\u0397","eta":"\u03B7","ETH":"\u00D0","eth":"\u00F0","Euml":"\u00CB","euml":"\u00EB","euro":"\u20AC","excl":"!","exist":"\u2203","Exists":"\u2203","expectation":"\u2130","exponentiale":"\u2147","ExponentialE":"\u2147","fallingdotseq":"\u2252","Fcy":"\u0424","fcy":"\u0444","female":"\u2640","ffilig":"\uFB03","fflig":"\uFB00","ffllig":"\uFB04","Ffr":"\uD835\uDD09","ffr":"\uD835\uDD23","filig":"\uFB01","FilledSmallSquare":"\u25FC","FilledVerySmallSquare":"\u25AA","fjlig":"fj","flat":"\u266D","fllig":"\uFB02","fltns":"\u25B1","fnof":"\u0192","Fopf":"\uD835\uDD3D","fopf":"\uD835\uDD57","forall":"\u2200","ForAll":"\u2200","fork":"\u22D4","forkv":"\u2AD9","Fouriertrf":"\u2131","fpartint":"\u2A0D","frac12":"\u00BD","frac13":"\u2153","frac14":"\u00BC","frac15":"\u2155","frac16":"\u2159","frac18":"\u215B","frac23":"\u2154","frac25":"\u2156","frac34":"\u00BE","frac35":"\u2157","frac38":"\u215C","frac45":"\u2158","frac56":"\u215A","frac58":"\u215D","frac78":"\u215E","frasl":"\u2044","frown":"\u2322","fscr":"\uD835\uDCBB","Fscr":"\u2131","gacute":"\u01F5","Gamma":"\u0393","gamma":"\u03B3","Gammad":"\u03DC","gammad":"\u03DD","gap":"\u2A86","Gbreve":"\u011E","gbreve":"\u011F","Gcedil":"\u0122","Gcirc":"\u011C","gcirc":"\u011D","Gcy":"\u0413","gcy":"\u0433","Gdot":"\u0120","gdot":"\u0121","ge":"\u2265","gE":"\u2267","gEl":"\u2A8C","gel":"\u22DB","geq":"\u2265","geqq":"\u2267","geqslant":"\u2A7E","gescc":"\u2AA9","ges":"\u2A7E","gesdot":"\u2A80","gesdoto":"\u2A82","gesdotol":"\u2A84","gesl":"\u22DB\uFE00","gesles":"\u2A94","Gfr":"\uD835\uDD0A","gfr":"\uD835\uDD24","gg":"\u226B","Gg":"\u22D9","ggg":"\u22D9","gimel":"\u2137","GJcy":"\u0403","gjcy":"\u0453","gla":"\u2AA5","gl":"\u2277","glE":"\u2A92","glj":"\u2AA4","gnap":"\u2A8A","gnapprox":"\u2A8A","gne":"\u2A88","gnE":"\u2269","gneq":"\u2A88","gneqq":"\u2269","gnsim":"\u22E7","Gopf":"\uD835\uDD3E","gopf":"\uD835\uDD58","grave":"`","GreaterEqual":"\u2265","GreaterEqualLess":"\u22DB","GreaterFullEqual":"\u2267","GreaterGreater":"\u2AA2","GreaterLess":"\u2277","GreaterSlantEqual":"\u2A7E","GreaterTilde":"\u2273","Gscr":"\uD835\uDCA2","gscr":"\u210A","gsim":"\u2273","gsime":"\u2A8E","gsiml":"\u2A90","gtcc":"\u2AA7","gtcir":"\u2A7A","gt":">","GT":">","Gt":"\u226B","gtdot":"\u22D7","gtlPar":"\u2995","gtquest":"\u2A7C","gtrapprox":"\u2A86","gtrarr":"\u2978","gtrdot":"\u22D7","gtreqless":"\u22DB","gtreqqless":"\u2A8C","gtrless":"\u2277","gtrsim":"\u2273","gvertneqq":"\u2269\uFE00","gvnE":"\u2269\uFE00","Hacek":"\u02C7","hairsp":"\u200A","half":"\u00BD","hamilt":"\u210B","HARDcy":"\u042A","hardcy":"\u044A","harrcir":"\u2948","harr":"\u2194","hArr":"\u21D4","harrw":"\u21AD","Hat":"^","hbar":"\u210F","Hcirc":"\u0124","hcirc":"\u0125","hearts":"\u2665","heartsuit":"\u2665","hellip":"\u2026","hercon":"\u22B9","hfr":"\uD835\uDD25","Hfr":"\u210C","HilbertSpace":"\u210B","hksearow":"\u2925","hkswarow":"\u2926","hoarr":"\u21FF","homtht":"\u223B","hookleftarrow":"\u21A9","hookrightarrow":"\u21AA","hopf":"\uD835\uDD59","Hopf":"\u210D","horbar":"\u2015","HorizontalLine":"\u2500","hscr":"\uD835\uDCBD","Hscr":"\u210B","hslash":"\u210F","Hstrok":"\u0126","hstrok":"\u0127","HumpDownHump":"\u224E","HumpEqual":"\u224F","hybull":"\u2043","hyphen":"\u2010","Iacute":"\u00CD","iacute":"\u00ED","ic":"\u2063","Icirc":"\u00CE","icirc":"\u00EE","Icy":"\u0418","icy":"\u0438","Idot":"\u0130","IEcy":"\u0415","iecy":"\u0435","iexcl":"\u00A1","iff":"\u21D4","ifr":"\uD835\uDD26","Ifr":"\u2111","Igrave":"\u00CC","igrave":"\u00EC","ii":"\u2148","iiiint":"\u2A0C","iiint":"\u222D","iinfin":"\u29DC","iiota":"\u2129","IJlig":"\u0132","ijlig":"\u0133","Imacr":"\u012A","imacr":"\u012B","image":"\u2111","ImaginaryI":"\u2148","imagline":"\u2110","imagpart":"\u2111","imath":"\u0131","Im":"\u2111","imof":"\u22B7","imped":"\u01B5","Implies":"\u21D2","incare":"\u2105","in":"\u2208","infin":"\u221E","infintie":"\u29DD","inodot":"\u0131","intcal":"\u22BA","int":"\u222B","Int":"\u222C","integers":"\u2124","Integral":"\u222B","intercal":"\u22BA","Intersection":"\u22C2","intlarhk":"\u2A17","intprod":"\u2A3C","InvisibleComma":"\u2063","InvisibleTimes":"\u2062","IOcy":"\u0401","iocy":"\u0451","Iogon":"\u012E","iogon":"\u012F","Iopf":"\uD835\uDD40","iopf":"\uD835\uDD5A","Iota":"\u0399","iota":"\u03B9","iprod":"\u2A3C","iquest":"\u00BF","iscr":"\uD835\uDCBE","Iscr":"\u2110","isin":"\u2208","isindot":"\u22F5","isinE":"\u22F9","isins":"\u22F4","isinsv":"\u22F3","isinv":"\u2208","it":"\u2062","Itilde":"\u0128","itilde":"\u0129","Iukcy":"\u0406","iukcy":"\u0456","Iuml":"\u00CF","iuml":"\u00EF","Jcirc":"\u0134","jcirc":"\u0135","Jcy":"\u0419","jcy":"\u0439","Jfr":"\uD835\uDD0D","jfr":"\uD835\uDD27","jmath":"\u0237","Jopf":"\uD835\uDD41","jopf":"\uD835\uDD5B","Jscr":"\uD835\uDCA5","jscr":"\uD835\uDCBF","Jsercy":"\u0408","jsercy":"\u0458","Jukcy":"\u0404","jukcy":"\u0454","Kappa":"\u039A","kappa":"\u03BA","kappav":"\u03F0","Kcedil":"\u0136","kcedil":"\u0137","Kcy":"\u041A","kcy":"\u043A","Kfr":"\uD835\uDD0E","kfr":"\uD835\uDD28","kgreen":"\u0138","KHcy":"\u0425","khcy":"\u0445","KJcy":"\u040C","kjcy":"\u045C","Kopf":"\uD835\uDD42","kopf":"\uD835\uDD5C","Kscr":"\uD835\uDCA6","kscr":"\uD835\uDCC0","lAarr":"\u21DA","Lacute":"\u0139","lacute":"\u013A","laemptyv":"\u29B4","lagran":"\u2112","Lambda":"\u039B","lambda":"\u03BB","lang":"\u27E8","Lang":"\u27EA","langd":"\u2991","langle":"\u27E8","lap":"\u2A85","Laplacetrf":"\u2112","laquo":"\u00AB","larrb":"\u21E4","larrbfs":"\u291F","larr":"\u2190","Larr":"\u219E","lArr":"\u21D0","larrfs":"\u291D","larrhk":"\u21A9","larrlp":"\u21AB","larrpl":"\u2939","larrsim":"\u2973","larrtl":"\u21A2","latail":"\u2919","lAtail":"\u291B","lat":"\u2AAB","late":"\u2AAD","lates":"\u2AAD\uFE00","lbarr":"\u290C","lBarr":"\u290E","lbbrk":"\u2772","lbrace":"{","lbrack":"[","lbrke":"\u298B","lbrksld":"\u298F","lbrkslu":"\u298D","Lcaron":"\u013D","lcaron":"\u013E","Lcedil":"\u013B","lcedil":"\u013C","lceil":"\u2308","lcub":"{","Lcy":"\u041B","lcy":"\u043B","ldca":"\u2936","ldquo":"\u201C","ldquor":"\u201E","ldrdhar":"\u2967","ldrushar":"\u294B","ldsh":"\u21B2","le":"\u2264","lE":"\u2266","LeftAngleBracket":"\u27E8","LeftArrowBar":"\u21E4","leftarrow":"\u2190","LeftArrow":"\u2190","Leftarrow":"\u21D0","LeftArrowRightArrow":"\u21C6","leftarrowtail":"\u21A2","LeftCeiling":"\u2308","LeftDoubleBracket":"\u27E6","LeftDownTeeVector":"\u2961","LeftDownVectorBar":"\u2959","LeftDownVector":"\u21C3","LeftFloor":"\u230A","leftharpoondown":"\u21BD","leftharpoonup":"\u21BC","leftleftarrows":"\u21C7","leftrightarrow":"\u2194","LeftRightArrow":"\u2194","Leftrightarrow":"\u21D4","leftrightarrows":"\u21C6","leftrightharpoons":"\u21CB","leftrightsquigarrow":"\u21AD","LeftRightVector":"\u294E","LeftTeeArrow":"\u21A4","LeftTee":"\u22A3","LeftTeeVector":"\u295A","leftthreetimes":"\u22CB","LeftTriangleBar":"\u29CF","LeftTriangle":"\u22B2","LeftTriangleEqual":"\u22B4","LeftUpDownVector":"\u2951","LeftUpTeeVector":"\u2960","LeftUpVectorBar":"\u2958","LeftUpVector":"\u21BF","LeftVectorBar":"\u2952","LeftVector":"\u21BC","lEg":"\u2A8B","leg":"\u22DA","leq":"\u2264","leqq":"\u2266","leqslant":"\u2A7D","lescc":"\u2AA8","les":"\u2A7D","lesdot":"\u2A7F","lesdoto":"\u2A81","lesdotor":"\u2A83","lesg":"\u22DA\uFE00","lesges":"\u2A93","lessapprox":"\u2A85","lessdot":"\u22D6","lesseqgtr":"\u22DA","lesseqqgtr":"\u2A8B","LessEqualGreater":"\u22DA","LessFullEqual":"\u2266","LessGreater":"\u2276","lessgtr":"\u2276","LessLess":"\u2AA1","lesssim":"\u2272","LessSlantEqual":"\u2A7D","LessTilde":"\u2272","lfisht":"\u297C","lfloor":"\u230A","Lfr":"\uD835\uDD0F","lfr":"\uD835\uDD29","lg":"\u2276","lgE":"\u2A91","lHar":"\u2962","lhard":"\u21BD","lharu":"\u21BC","lharul":"\u296A","lhblk":"\u2584","LJcy":"\u0409","ljcy":"\u0459","llarr":"\u21C7","ll":"\u226A","Ll":"\u22D8","llcorner":"\u231E","Lleftarrow":"\u21DA","llhard":"\u296B","lltri":"\u25FA","Lmidot":"\u013F","lmidot":"\u0140","lmoustache":"\u23B0","lmoust":"\u23B0","lnap":"\u2A89","lnapprox":"\u2A89","lne":"\u2A87","lnE":"\u2268","lneq":"\u2A87","lneqq":"\u2268","lnsim":"\u22E6","loang":"\u27EC","loarr":"\u21FD","lobrk":"\u27E6","longleftarrow":"\u27F5","LongLeftArrow":"\u27F5","Longleftarrow":"\u27F8","longleftrightarrow":"\u27F7","LongLeftRightArrow":"\u27F7","Longleftrightarrow":"\u27FA","longmapsto":"\u27FC","longrightarrow":"\u27F6","LongRightArrow":"\u27F6","Longrightarrow":"\u27F9","looparrowleft":"\u21AB","looparrowright":"\u21AC","lopar":"\u2985","Lopf":"\uD835\uDD43","lopf":"\uD835\uDD5D","loplus":"\u2A2D","lotimes":"\u2A34","lowast":"\u2217","lowbar":"_","LowerLeftArrow":"\u2199","LowerRightArrow":"\u2198","loz":"\u25CA","lozenge":"\u25CA","lozf":"\u29EB","lpar":"(","lparlt":"\u2993","lrarr":"\u21C6","lrcorner":"\u231F","lrhar":"\u21CB","lrhard":"\u296D","lrm":"\u200E","lrtri":"\u22BF","lsaquo":"\u2039","lscr":"\uD835\uDCC1","Lscr":"\u2112","lsh":"\u21B0","Lsh":"\u21B0","lsim":"\u2272","lsime":"\u2A8D","lsimg":"\u2A8F","lsqb":"[","lsquo":"\u2018","lsquor":"\u201A","Lstrok":"\u0141","lstrok":"\u0142","ltcc":"\u2AA6","ltcir":"\u2A79","lt":"<","LT":"<","Lt":"\u226A","ltdot":"\u22D6","lthree":"\u22CB","ltimes":"\u22C9","ltlarr":"\u2976","ltquest":"\u2A7B","ltri":"\u25C3","ltrie":"\u22B4","ltrif":"\u25C2","ltrPar":"\u2996","lurdshar":"\u294A","luruhar":"\u2966","lvertneqq":"\u2268\uFE00","lvnE":"\u2268\uFE00","macr":"\u00AF","male":"\u2642","malt":"\u2720","maltese":"\u2720","Map":"\u2905","map":"\u21A6","mapsto":"\u21A6","mapstodown":"\u21A7","mapstoleft":"\u21A4","mapstoup":"\u21A5","marker":"\u25AE","mcomma":"\u2A29","Mcy":"\u041C","mcy":"\u043C","mdash":"\u2014","mDDot":"\u223A","measuredangle":"\u2221","MediumSpace":"\u205F","Mellintrf":"\u2133","Mfr":"\uD835\uDD10","mfr":"\uD835\uDD2A","mho":"\u2127","micro":"\u00B5","midast":"*","midcir":"\u2AF0","mid":"\u2223","middot":"\u00B7","minusb":"\u229F","minus":"\u2212","minusd":"\u2238","minusdu":"\u2A2A","MinusPlus":"\u2213","mlcp":"\u2ADB","mldr":"\u2026","mnplus":"\u2213","models":"\u22A7","Mopf":"\uD835\uDD44","mopf":"\uD835\uDD5E","mp":"\u2213","mscr":"\uD835\uDCC2","Mscr":"\u2133","mstpos":"\u223E","Mu":"\u039C","mu":"\u03BC","multimap":"\u22B8","mumap":"\u22B8","nabla":"\u2207","Nacute":"\u0143","nacute":"\u0144","nang":"\u2220\u20D2","nap":"\u2249","napE":"\u2A70\u0338","napid":"\u224B\u0338","napos":"\u0149","napprox":"\u2249","natural":"\u266E","naturals":"\u2115","natur":"\u266E","nbsp":"\u00A0","nbump":"\u224E\u0338","nbumpe":"\u224F\u0338","ncap":"\u2A43","Ncaron":"\u0147","ncaron":"\u0148","Ncedil":"\u0145","ncedil":"\u0146","ncong":"\u2247","ncongdot":"\u2A6D\u0338","ncup":"\u2A42","Ncy":"\u041D","ncy":"\u043D","ndash":"\u2013","nearhk":"\u2924","nearr":"\u2197","neArr":"\u21D7","nearrow":"\u2197","ne":"\u2260","nedot":"\u2250\u0338","NegativeMediumSpace":"\u200B","NegativeThickSpace":"\u200B","NegativeThinSpace":"\u200B","NegativeVeryThinSpace":"\u200B","nequiv":"\u2262","nesear":"\u2928","nesim":"\u2242\u0338","NestedGreaterGreater":"\u226B","NestedLessLess":"\u226A","NewLine":"\n","nexist":"\u2204","nexists":"\u2204","Nfr":"\uD835\uDD11","nfr":"\uD835\uDD2B","ngE":"\u2267\u0338","nge":"\u2271","ngeq":"\u2271","ngeqq":"\u2267\u0338","ngeqslant":"\u2A7E\u0338","nges":"\u2A7E\u0338","nGg":"\u22D9\u0338","ngsim":"\u2275","nGt":"\u226B\u20D2","ngt":"\u226F","ngtr":"\u226F","nGtv":"\u226B\u0338","nharr":"\u21AE","nhArr":"\u21CE","nhpar":"\u2AF2","ni":"\u220B","nis":"\u22FC","nisd":"\u22FA","niv":"\u220B","NJcy":"\u040A","njcy":"\u045A","nlarr":"\u219A","nlArr":"\u21CD","nldr":"\u2025","nlE":"\u2266\u0338","nle":"\u2270","nleftarrow":"\u219A","nLeftarrow":"\u21CD","nleftrightarrow":"\u21AE","nLeftrightarrow":"\u21CE","nleq":"\u2270","nleqq":"\u2266\u0338","nleqslant":"\u2A7D\u0338","nles":"\u2A7D\u0338","nless":"\u226E","nLl":"\u22D8\u0338","nlsim":"\u2274","nLt":"\u226A\u20D2","nlt":"\u226E","nltri":"\u22EA","nltrie":"\u22EC","nLtv":"\u226A\u0338","nmid":"\u2224","NoBreak":"\u2060","NonBreakingSpace":"\u00A0","nopf":"\uD835\uDD5F","Nopf":"\u2115","Not":"\u2AEC","not":"\u00AC","NotCongruent":"\u2262","NotCupCap":"\u226D","NotDoubleVerticalBar":"\u2226","NotElement":"\u2209","NotEqual":"\u2260","NotEqualTilde":"\u2242\u0338","NotExists":"\u2204","NotGreater":"\u226F","NotGreaterEqual":"\u2271","NotGreaterFullEqual":"\u2267\u0338","NotGreaterGreater":"\u226B\u0338","NotGreaterLess":"\u2279","NotGreaterSlantEqual":"\u2A7E\u0338","NotGreaterTilde":"\u2275","NotHumpDownHump":"\u224E\u0338","NotHumpEqual":"\u224F\u0338","notin":"\u2209","notindot":"\u22F5\u0338","notinE":"\u22F9\u0338","notinva":"\u2209","notinvb":"\u22F7","notinvc":"\u22F6","NotLeftTriangleBar":"\u29CF\u0338","NotLeftTriangle":"\u22EA","NotLeftTriangleEqual":"\u22EC","NotLess":"\u226E","NotLessEqual":"\u2270","NotLessGreater":"\u2278","NotLessLess":"\u226A\u0338","NotLessSlantEqual":"\u2A7D\u0338","NotLessTilde":"\u2274","NotNestedGreaterGreater":"\u2AA2\u0338","NotNestedLessLess":"\u2AA1\u0338","notni":"\u220C","notniva":"\u220C","notnivb":"\u22FE","notnivc":"\u22FD","NotPrecedes":"\u2280","NotPrecedesEqual":"\u2AAF\u0338","NotPrecedesSlantEqual":"\u22E0","NotReverseElement":"\u220C","NotRightTriangleBar":"\u29D0\u0338","NotRightTriangle":"\u22EB","NotRightTriangleEqual":"\u22ED","NotSquareSubset":"\u228F\u0338","NotSquareSubsetEqual":"\u22E2","NotSquareSuperset":"\u2290\u0338","NotSquareSupersetEqual":"\u22E3","NotSubset":"\u2282\u20D2","NotSubsetEqual":"\u2288","NotSucceeds":"\u2281","NotSucceedsEqual":"\u2AB0\u0338","NotSucceedsSlantEqual":"\u22E1","NotSucceedsTilde":"\u227F\u0338","NotSuperset":"\u2283\u20D2","NotSupersetEqual":"\u2289","NotTilde":"\u2241","NotTildeEqual":"\u2244","NotTildeFullEqual":"\u2247","NotTildeTilde":"\u2249","NotVerticalBar":"\u2224","nparallel":"\u2226","npar":"\u2226","nparsl":"\u2AFD\u20E5","npart":"\u2202\u0338","npolint":"\u2A14","npr":"\u2280","nprcue":"\u22E0","nprec":"\u2280","npreceq":"\u2AAF\u0338","npre":"\u2AAF\u0338","nrarrc":"\u2933\u0338","nrarr":"\u219B","nrArr":"\u21CF","nrarrw":"\u219D\u0338","nrightarrow":"\u219B","nRightarrow":"\u21CF","nrtri":"\u22EB","nrtrie":"\u22ED","nsc":"\u2281","nsccue":"\u22E1","nsce":"\u2AB0\u0338","Nscr":"\uD835\uDCA9","nscr":"\uD835\uDCC3","nshortmid":"\u2224","nshortparallel":"\u2226","nsim":"\u2241","nsime":"\u2244","nsimeq":"\u2244","nsmid":"\u2224","nspar":"\u2226","nsqsube":"\u22E2","nsqsupe":"\u22E3","nsub":"\u2284","nsubE":"\u2AC5\u0338","nsube":"\u2288","nsubset":"\u2282\u20D2","nsubseteq":"\u2288","nsubseteqq":"\u2AC5\u0338","nsucc":"\u2281","nsucceq":"\u2AB0\u0338","nsup":"\u2285","nsupE":"\u2AC6\u0338","nsupe":"\u2289","nsupset":"\u2283\u20D2","nsupseteq":"\u2289","nsupseteqq":"\u2AC6\u0338","ntgl":"\u2279","Ntilde":"\u00D1","ntilde":"\u00F1","ntlg":"\u2278","ntriangleleft":"\u22EA","ntrianglelefteq":"\u22EC","ntriangleright":"\u22EB","ntrianglerighteq":"\u22ED","Nu":"\u039D","nu":"\u03BD","num":"#","numero":"\u2116","numsp":"\u2007","nvap":"\u224D\u20D2","nvdash":"\u22AC","nvDash":"\u22AD","nVdash":"\u22AE","nVDash":"\u22AF","nvge":"\u2265\u20D2","nvgt":">\u20D2","nvHarr":"\u2904","nvinfin":"\u29DE","nvlArr":"\u2902","nvle":"\u2264\u20D2","nvlt":"<\u20D2","nvltrie":"\u22B4\u20D2","nvrArr":"\u2903","nvrtrie":"\u22B5\u20D2","nvsim":"\u223C\u20D2","nwarhk":"\u2923","nwarr":"\u2196","nwArr":"\u21D6","nwarrow":"\u2196","nwnear":"\u2927","Oacute":"\u00D3","oacute":"\u00F3","oast":"\u229B","Ocirc":"\u00D4","ocirc":"\u00F4","ocir":"\u229A","Ocy":"\u041E","ocy":"\u043E","odash":"\u229D","Odblac":"\u0150","odblac":"\u0151","odiv":"\u2A38","odot":"\u2299","odsold":"\u29BC","OElig":"\u0152","oelig":"\u0153","ofcir":"\u29BF","Ofr":"\uD835\uDD12","ofr":"\uD835\uDD2C","ogon":"\u02DB","Ograve":"\u00D2","ograve":"\u00F2","ogt":"\u29C1","ohbar":"\u29B5","ohm":"\u03A9","oint":"\u222E","olarr":"\u21BA","olcir":"\u29BE","olcross":"\u29BB","oline":"\u203E","olt":"\u29C0","Omacr":"\u014C","omacr":"\u014D","Omega":"\u03A9","omega":"\u03C9","Omicron":"\u039F","omicron":"\u03BF","omid":"\u29B6","ominus":"\u2296","Oopf":"\uD835\uDD46","oopf":"\uD835\uDD60","opar":"\u29B7","OpenCurlyDoubleQuote":"\u201C","OpenCurlyQuote":"\u2018","operp":"\u29B9","oplus":"\u2295","orarr":"\u21BB","Or":"\u2A54","or":"\u2228","ord":"\u2A5D","order":"\u2134","orderof":"\u2134","ordf":"\u00AA","ordm":"\u00BA","origof":"\u22B6","oror":"\u2A56","orslope":"\u2A57","orv":"\u2A5B","oS":"\u24C8","Oscr":"\uD835\uDCAA","oscr":"\u2134","Oslash":"\u00D8","oslash":"\u00F8","osol":"\u2298","Otilde":"\u00D5","otilde":"\u00F5","otimesas":"\u2A36","Otimes":"\u2A37","otimes":"\u2297","Ouml":"\u00D6","ouml":"\u00F6","ovbar":"\u233D","OverBar":"\u203E","OverBrace":"\u23DE","OverBracket":"\u23B4","OverParenthesis":"\u23DC","para":"\u00B6","parallel":"\u2225","par":"\u2225","parsim":"\u2AF3","parsl":"\u2AFD","part":"\u2202","PartialD":"\u2202","Pcy":"\u041F","pcy":"\u043F","percnt":"%","period":".","permil":"\u2030","perp":"\u22A5","pertenk":"\u2031","Pfr":"\uD835\uDD13","pfr":"\uD835\uDD2D","Phi":"\u03A6","phi":"\u03C6","phiv":"\u03D5","phmmat":"\u2133","phone":"\u260E","Pi":"\u03A0","pi":"\u03C0","pitchfork":"\u22D4","piv":"\u03D6","planck":"\u210F","planckh":"\u210E","plankv":"\u210F","plusacir":"\u2A23","plusb":"\u229E","pluscir":"\u2A22","plus":"+","plusdo":"\u2214","plusdu":"\u2A25","pluse":"\u2A72","PlusMinus":"\u00B1","plusmn":"\u00B1","plussim":"\u2A26","plustwo":"\u2A27","pm":"\u00B1","Poincareplane":"\u210C","pointint":"\u2A15","popf":"\uD835\uDD61","Popf":"\u2119","pound":"\u00A3","prap":"\u2AB7","Pr":"\u2ABB","pr":"\u227A","prcue":"\u227C","precapprox":"\u2AB7","prec":"\u227A","preccurlyeq":"\u227C","Precedes":"\u227A","PrecedesEqual":"\u2AAF","PrecedesSlantEqual":"\u227C","PrecedesTilde":"\u227E","preceq":"\u2AAF","precnapprox":"\u2AB9","precneqq":"\u2AB5","precnsim":"\u22E8","pre":"\u2AAF","prE":"\u2AB3","precsim":"\u227E","prime":"\u2032","Prime":"\u2033","primes":"\u2119","prnap":"\u2AB9","prnE":"\u2AB5","prnsim":"\u22E8","prod":"\u220F","Product":"\u220F","profalar":"\u232E","profline":"\u2312","profsurf":"\u2313","prop":"\u221D","Proportional":"\u221D","Proportion":"\u2237","propto":"\u221D","prsim":"\u227E","prurel":"\u22B0","Pscr":"\uD835\uDCAB","pscr":"\uD835\uDCC5","Psi":"\u03A8","psi":"\u03C8","puncsp":"\u2008","Qfr":"\uD835\uDD14","qfr":"\uD835\uDD2E","qint":"\u2A0C","qopf":"\uD835\uDD62","Qopf":"\u211A","qprime":"\u2057","Qscr":"\uD835\uDCAC","qscr":"\uD835\uDCC6","quaternions":"\u210D","quatint":"\u2A16","quest":"?","questeq":"\u225F","quot":"\"","QUOT":"\"","rAarr":"\u21DB","race":"\u223D\u0331","Racute":"\u0154","racute":"\u0155","radic":"\u221A","raemptyv":"\u29B3","rang":"\u27E9","Rang":"\u27EB","rangd":"\u2992","range":"\u29A5","rangle":"\u27E9","raquo":"\u00BB","rarrap":"\u2975","rarrb":"\u21E5","rarrbfs":"\u2920","rarrc":"\u2933","rarr":"\u2192","Rarr":"\u21A0","rArr":"\u21D2","rarrfs":"\u291E","rarrhk":"\u21AA","rarrlp":"\u21AC","rarrpl":"\u2945","rarrsim":"\u2974","Rarrtl":"\u2916","rarrtl":"\u21A3","rarrw":"\u219D","ratail":"\u291A","rAtail":"\u291C","ratio":"\u2236","rationals":"\u211A","rbarr":"\u290D","rBarr":"\u290F","RBarr":"\u2910","rbbrk":"\u2773","rbrace":"}","rbrack":"]","rbrke":"\u298C","rbrksld":"\u298E","rbrkslu":"\u2990","Rcaron":"\u0158","rcaron":"\u0159","Rcedil":"\u0156","rcedil":"\u0157","rceil":"\u2309","rcub":"}","Rcy":"\u0420","rcy":"\u0440","rdca":"\u2937","rdldhar":"\u2969","rdquo":"\u201D","rdquor":"\u201D","rdsh":"\u21B3","real":"\u211C","realine":"\u211B","realpart":"\u211C","reals":"\u211D","Re":"\u211C","rect":"\u25AD","reg":"\u00AE","REG":"\u00AE","ReverseElement":"\u220B","ReverseEquilibrium":"\u21CB","ReverseUpEquilibrium":"\u296F","rfisht":"\u297D","rfloor":"\u230B","rfr":"\uD835\uDD2F","Rfr":"\u211C","rHar":"\u2964","rhard":"\u21C1","rharu":"\u21C0","rharul":"\u296C","Rho":"\u03A1","rho":"\u03C1","rhov":"\u03F1","RightAngleBracket":"\u27E9","RightArrowBar":"\u21E5","rightarrow":"\u2192","RightArrow":"\u2192","Rightarrow":"\u21D2","RightArrowLeftArrow":"\u21C4","rightarrowtail":"\u21A3","RightCeiling":"\u2309","RightDoubleBracket":"\u27E7","RightDownTeeVector":"\u295D","RightDownVectorBar":"\u2955","RightDownVector":"\u21C2","RightFloor":"\u230B","rightharpoondown":"\u21C1","rightharpoonup":"\u21C0","rightleftarrows":"\u21C4","rightleftharpoons":"\u21CC","rightrightarrows":"\u21C9","rightsquigarrow":"\u219D","RightTeeArrow":"\u21A6","RightTee":"\u22A2","RightTeeVector":"\u295B","rightthreetimes":"\u22CC","RightTriangleBar":"\u29D0","RightTriangle":"\u22B3","RightTriangleEqual":"\u22B5","RightUpDownVector":"\u294F","RightUpTeeVector":"\u295C","RightUpVectorBar":"\u2954","RightUpVector":"\u21BE","RightVectorBar":"\u2953","RightVector":"\u21C0","ring":"\u02DA","risingdotseq":"\u2253","rlarr":"\u21C4","rlhar":"\u21CC","rlm":"\u200F","rmoustache":"\u23B1","rmoust":"\u23B1","rnmid":"\u2AEE","roang":"\u27ED","roarr":"\u21FE","robrk":"\u27E7","ropar":"\u2986","ropf":"\uD835\uDD63","Ropf":"\u211D","roplus":"\u2A2E","rotimes":"\u2A35","RoundImplies":"\u2970","rpar":")","rpargt":"\u2994","rppolint":"\u2A12","rrarr":"\u21C9","Rrightarrow":"\u21DB","rsaquo":"\u203A","rscr":"\uD835\uDCC7","Rscr":"\u211B","rsh":"\u21B1","Rsh":"\u21B1","rsqb":"]","rsquo":"\u2019","rsquor":"\u2019","rthree":"\u22CC","rtimes":"\u22CA","rtri":"\u25B9","rtrie":"\u22B5","rtrif":"\u25B8","rtriltri":"\u29CE","RuleDelayed":"\u29F4","ruluhar":"\u2968","rx":"\u211E","Sacute":"\u015A","sacute":"\u015B","sbquo":"\u201A","scap":"\u2AB8","Scaron":"\u0160","scaron":"\u0161","Sc":"\u2ABC","sc":"\u227B","sccue":"\u227D","sce":"\u2AB0","scE":"\u2AB4","Scedil":"\u015E","scedil":"\u015F","Scirc":"\u015C","scirc":"\u015D","scnap":"\u2ABA","scnE":"\u2AB6","scnsim":"\u22E9","scpolint":"\u2A13","scsim":"\u227F","Scy":"\u0421","scy":"\u0441","sdotb":"\u22A1","sdot":"\u22C5","sdote":"\u2A66","searhk":"\u2925","searr":"\u2198","seArr":"\u21D8","searrow":"\u2198","sect":"\u00A7","semi":";","seswar":"\u2929","setminus":"\u2216","setmn":"\u2216","sext":"\u2736","Sfr":"\uD835\uDD16","sfr":"\uD835\uDD30","sfrown":"\u2322","sharp":"\u266F","SHCHcy":"\u0429","shchcy":"\u0449","SHcy":"\u0428","shcy":"\u0448","ShortDownArrow":"\u2193","ShortLeftArrow":"\u2190","shortmid":"\u2223","shortparallel":"\u2225","ShortRightArrow":"\u2192","ShortUpArrow":"\u2191","shy":"\u00AD","Sigma":"\u03A3","sigma":"\u03C3","sigmaf":"\u03C2","sigmav":"\u03C2","sim":"\u223C","simdot":"\u2A6A","sime":"\u2243","simeq":"\u2243","simg":"\u2A9E","simgE":"\u2AA0","siml":"\u2A9D","simlE":"\u2A9F","simne":"\u2246","simplus":"\u2A24","simrarr":"\u2972","slarr":"\u2190","SmallCircle":"\u2218","smallsetminus":"\u2216","smashp":"\u2A33","smeparsl":"\u29E4","smid":"\u2223","smile":"\u2323","smt":"\u2AAA","smte":"\u2AAC","smtes":"\u2AAC\uFE00","SOFTcy":"\u042C","softcy":"\u044C","solbar":"\u233F","solb":"\u29C4","sol":"/","Sopf":"\uD835\uDD4A","sopf":"\uD835\uDD64","spades":"\u2660","spadesuit":"\u2660","spar":"\u2225","sqcap":"\u2293","sqcaps":"\u2293\uFE00","sqcup":"\u2294","sqcups":"\u2294\uFE00","Sqrt":"\u221A","sqsub":"\u228F","sqsube":"\u2291","sqsubset":"\u228F","sqsubseteq":"\u2291","sqsup":"\u2290","sqsupe":"\u2292","sqsupset":"\u2290","sqsupseteq":"\u2292","square":"\u25A1","Square":"\u25A1","SquareIntersection":"\u2293","SquareSubset":"\u228F","SquareSubsetEqual":"\u2291","SquareSuperset":"\u2290","SquareSupersetEqual":"\u2292","SquareUnion":"\u2294","squarf":"\u25AA","squ":"\u25A1","squf":"\u25AA","srarr":"\u2192","Sscr":"\uD835\uDCAE","sscr":"\uD835\uDCC8","ssetmn":"\u2216","ssmile":"\u2323","sstarf":"\u22C6","Star":"\u22C6","star":"\u2606","starf":"\u2605","straightepsilon":"\u03F5","straightphi":"\u03D5","strns":"\u00AF","sub":"\u2282","Sub":"\u22D0","subdot":"\u2ABD","subE":"\u2AC5","sube":"\u2286","subedot":"\u2AC3","submult":"\u2AC1","subnE":"\u2ACB","subne":"\u228A","subplus":"\u2ABF","subrarr":"\u2979","subset":"\u2282","Subset":"\u22D0","subseteq":"\u2286","subseteqq":"\u2AC5","SubsetEqual":"\u2286","subsetneq":"\u228A","subsetneqq":"\u2ACB","subsim":"\u2AC7","subsub":"\u2AD5","subsup":"\u2AD3","succapprox":"\u2AB8","succ":"\u227B","succcurlyeq":"\u227D","Succeeds":"\u227B","SucceedsEqual":"\u2AB0","SucceedsSlantEqual":"\u227D","SucceedsTilde":"\u227F","succeq":"\u2AB0","succnapprox":"\u2ABA","succneqq":"\u2AB6","succnsim":"\u22E9","succsim":"\u227F","SuchThat":"\u220B","sum":"\u2211","Sum":"\u2211","sung":"\u266A","sup1":"\u00B9","sup2":"\u00B2","sup3":"\u00B3","sup":"\u2283","Sup":"\u22D1","supdot":"\u2ABE","supdsub":"\u2AD8","supE":"\u2AC6","supe":"\u2287","supedot":"\u2AC4","Superset":"\u2283","SupersetEqual":"\u2287","suphsol":"\u27C9","suphsub":"\u2AD7","suplarr":"\u297B","supmult":"\u2AC2","supnE":"\u2ACC","supne":"\u228B","supplus":"\u2AC0","supset":"\u2283","Supset":"\u22D1","supseteq":"\u2287","supseteqq":"\u2AC6","supsetneq":"\u228B","supsetneqq":"\u2ACC","supsim":"\u2AC8","supsub":"\u2AD4","supsup":"\u2AD6","swarhk":"\u2926","swarr":"\u2199","swArr":"\u21D9","swarrow":"\u2199","swnwar":"\u292A","szlig":"\u00DF","Tab":"\t","target":"\u2316","Tau":"\u03A4","tau":"\u03C4","tbrk":"\u23B4","Tcaron":"\u0164","tcaron":"\u0165","Tcedil":"\u0162","tcedil":"\u0163","Tcy":"\u0422","tcy":"\u0442","tdot":"\u20DB","telrec":"\u2315","Tfr":"\uD835\uDD17","tfr":"\uD835\uDD31","there4":"\u2234","therefore":"\u2234","Therefore":"\u2234","Theta":"\u0398","theta":"\u03B8","thetasym":"\u03D1","thetav":"\u03D1","thickapprox":"\u2248","thicksim":"\u223C","ThickSpace":"\u205F\u200A","ThinSpace":"\u2009","thinsp":"\u2009","thkap":"\u2248","thksim":"\u223C","THORN":"\u00DE","thorn":"\u00FE","tilde":"\u02DC","Tilde":"\u223C","TildeEqual":"\u2243","TildeFullEqual":"\u2245","TildeTilde":"\u2248","timesbar":"\u2A31","timesb":"\u22A0","times":"\u00D7","timesd":"\u2A30","tint":"\u222D","toea":"\u2928","topbot":"\u2336","topcir":"\u2AF1","top":"\u22A4","Topf":"\uD835\uDD4B","topf":"\uD835\uDD65","topfork":"\u2ADA","tosa":"\u2929","tprime":"\u2034","trade":"\u2122","TRADE":"\u2122","triangle":"\u25B5","triangledown":"\u25BF","triangleleft":"\u25C3","trianglelefteq":"\u22B4","triangleq":"\u225C","triangleright":"\u25B9","trianglerighteq":"\u22B5","tridot":"\u25EC","trie":"\u225C","triminus":"\u2A3A","TripleDot":"\u20DB","triplus":"\u2A39","trisb":"\u29CD","tritime":"\u2A3B","trpezium":"\u23E2","Tscr":"\uD835\uDCAF","tscr":"\uD835\uDCC9","TScy":"\u0426","tscy":"\u0446","TSHcy":"\u040B","tshcy":"\u045B","Tstrok":"\u0166","tstrok":"\u0167","twixt":"\u226C","twoheadleftarrow":"\u219E","twoheadrightarrow":"\u21A0","Uacute":"\u00DA","uacute":"\u00FA","uarr":"\u2191","Uarr":"\u219F","uArr":"\u21D1","Uarrocir":"\u2949","Ubrcy":"\u040E","ubrcy":"\u045E","Ubreve":"\u016C","ubreve":"\u016D","Ucirc":"\u00DB","ucirc":"\u00FB","Ucy":"\u0423","ucy":"\u0443","udarr":"\u21C5","Udblac":"\u0170","udblac":"\u0171","udhar":"\u296E","ufisht":"\u297E","Ufr":"\uD835\uDD18","ufr":"\uD835\uDD32","Ugrave":"\u00D9","ugrave":"\u00F9","uHar":"\u2963","uharl":"\u21BF","uharr":"\u21BE","uhblk":"\u2580","ulcorn":"\u231C","ulcorner":"\u231C","ulcrop":"\u230F","ultri":"\u25F8","Umacr":"\u016A","umacr":"\u016B","uml":"\u00A8","UnderBar":"_","UnderBrace":"\u23DF","UnderBracket":"\u23B5","UnderParenthesis":"\u23DD","Union":"\u22C3","UnionPlus":"\u228E","Uogon":"\u0172","uogon":"\u0173","Uopf":"\uD835\uDD4C","uopf":"\uD835\uDD66","UpArrowBar":"\u2912","uparrow":"\u2191","UpArrow":"\u2191","Uparrow":"\u21D1","UpArrowDownArrow":"\u21C5","updownarrow":"\u2195","UpDownArrow":"\u2195","Updownarrow":"\u21D5","UpEquilibrium":"\u296E","upharpoonleft":"\u21BF","upharpoonright":"\u21BE","uplus":"\u228E","UpperLeftArrow":"\u2196","UpperRightArrow":"\u2197","upsi":"\u03C5","Upsi":"\u03D2","upsih":"\u03D2","Upsilon":"\u03A5","upsilon":"\u03C5","UpTeeArrow":"\u21A5","UpTee":"\u22A5","upuparrows":"\u21C8","urcorn":"\u231D","urcorner":"\u231D","urcrop":"\u230E","Uring":"\u016E","uring":"\u016F","urtri":"\u25F9","Uscr":"\uD835\uDCB0","uscr":"\uD835\uDCCA","utdot":"\u22F0","Utilde":"\u0168","utilde":"\u0169","utri":"\u25B5","utrif":"\u25B4","uuarr":"\u21C8","Uuml":"\u00DC","uuml":"\u00FC","uwangle":"\u29A7","vangrt":"\u299C","varepsilon":"\u03F5","varkappa":"\u03F0","varnothing":"\u2205","varphi":"\u03D5","varpi":"\u03D6","varpropto":"\u221D","varr":"\u2195","vArr":"\u21D5","varrho":"\u03F1","varsigma":"\u03C2","varsubsetneq":"\u228A\uFE00","varsubsetneqq":"\u2ACB\uFE00","varsupsetneq":"\u228B\uFE00","varsupsetneqq":"\u2ACC\uFE00","vartheta":"\u03D1","vartriangleleft":"\u22B2","vartriangleright":"\u22B3","vBar":"\u2AE8","Vbar":"\u2AEB","vBarv":"\u2AE9","Vcy":"\u0412","vcy":"\u0432","vdash":"\u22A2","vDash":"\u22A8","Vdash":"\u22A9","VDash":"\u22AB","Vdashl":"\u2AE6","veebar":"\u22BB","vee":"\u2228","Vee":"\u22C1","veeeq":"\u225A","vellip":"\u22EE","verbar":"|","Verbar":"\u2016","vert":"|","Vert":"\u2016","VerticalBar":"\u2223","VerticalLine":"|","VerticalSeparator":"\u2758","VerticalTilde":"\u2240","VeryThinSpace":"\u200A","Vfr":"\uD835\uDD19","vfr":"\uD835\uDD33","vltri":"\u22B2","vnsub":"\u2282\u20D2","vnsup":"\u2283\u20D2","Vopf":"\uD835\uDD4D","vopf":"\uD835\uDD67","vprop":"\u221D","vrtri":"\u22B3","Vscr":"\uD835\uDCB1","vscr":"\uD835\uDCCB","vsubnE":"\u2ACB\uFE00","vsubne":"\u228A\uFE00","vsupnE":"\u2ACC\uFE00","vsupne":"\u228B\uFE00","Vvdash":"\u22AA","vzigzag":"\u299A","Wcirc":"\u0174","wcirc":"\u0175","wedbar":"\u2A5F","wedge":"\u2227","Wedge":"\u22C0","wedgeq":"\u2259","weierp":"\u2118","Wfr":"\uD835\uDD1A","wfr":"\uD835\uDD34","Wopf":"\uD835\uDD4E","wopf":"\uD835\uDD68","wp":"\u2118","wr":"\u2240","wreath":"\u2240","Wscr":"\uD835\uDCB2","wscr":"\uD835\uDCCC","xcap":"\u22C2","xcirc":"\u25EF","xcup":"\u22C3","xdtri":"\u25BD","Xfr":"\uD835\uDD1B","xfr":"\uD835\uDD35","xharr":"\u27F7","xhArr":"\u27FA","Xi":"\u039E","xi":"\u03BE","xlarr":"\u27F5","xlArr":"\u27F8","xmap":"\u27FC","xnis":"\u22FB","xodot":"\u2A00","Xopf":"\uD835\uDD4F","xopf":"\uD835\uDD69","xoplus":"\u2A01","xotime":"\u2A02","xrarr":"\u27F6","xrArr":"\u27F9","Xscr":"\uD835\uDCB3","xscr":"\uD835\uDCCD","xsqcup":"\u2A06","xuplus":"\u2A04","xutri":"\u25B3","xvee":"\u22C1","xwedge":"\u22C0","Yacute":"\u00DD","yacute":"\u00FD","YAcy":"\u042F","yacy":"\u044F","Ycirc":"\u0176","ycirc":"\u0177","Ycy":"\u042B","ycy":"\u044B","yen":"\u00A5","Yfr":"\uD835\uDD1C","yfr":"\uD835\uDD36","YIcy":"\u0407","yicy":"\u0457","Yopf":"\uD835\uDD50","yopf":"\uD835\uDD6A","Yscr":"\uD835\uDCB4","yscr":"\uD835\uDCCE","YUcy":"\u042E","yucy":"\u044E","yuml":"\u00FF","Yuml":"\u0178","Zacute":"\u0179","zacute":"\u017A","Zcaron":"\u017D","zcaron":"\u017E","Zcy":"\u0417","zcy":"\u0437","Zdot":"\u017B","zdot":"\u017C","zeetrf":"\u2128","ZeroWidthSpace":"\u200B","Zeta":"\u0396","zeta":"\u03B6","zfr":"\uD835\uDD37","Zfr":"\u2128","ZHcy":"\u0416","zhcy":"\u0436","zigrarr":"\u21DD","zopf":"\uD835\uDD6B","Zopf":"\u2124","Zscr":"\uD835\uDCB5","zscr":"\uD835\uDCCF","zwj":"\u200D","zwnj":"\u200C"} +},{}],17:[function(require,module,exports){ +module.exports={"Aacute":"\u00C1","aacute":"\u00E1","Acirc":"\u00C2","acirc":"\u00E2","acute":"\u00B4","AElig":"\u00C6","aelig":"\u00E6","Agrave":"\u00C0","agrave":"\u00E0","amp":"&","AMP":"&","Aring":"\u00C5","aring":"\u00E5","Atilde":"\u00C3","atilde":"\u00E3","Auml":"\u00C4","auml":"\u00E4","brvbar":"\u00A6","Ccedil":"\u00C7","ccedil":"\u00E7","cedil":"\u00B8","cent":"\u00A2","copy":"\u00A9","COPY":"\u00A9","curren":"\u00A4","deg":"\u00B0","divide":"\u00F7","Eacute":"\u00C9","eacute":"\u00E9","Ecirc":"\u00CA","ecirc":"\u00EA","Egrave":"\u00C8","egrave":"\u00E8","ETH":"\u00D0","eth":"\u00F0","Euml":"\u00CB","euml":"\u00EB","frac12":"\u00BD","frac14":"\u00BC","frac34":"\u00BE","gt":">","GT":">","Iacute":"\u00CD","iacute":"\u00ED","Icirc":"\u00CE","icirc":"\u00EE","iexcl":"\u00A1","Igrave":"\u00CC","igrave":"\u00EC","iquest":"\u00BF","Iuml":"\u00CF","iuml":"\u00EF","laquo":"\u00AB","lt":"<","LT":"<","macr":"\u00AF","micro":"\u00B5","middot":"\u00B7","nbsp":"\u00A0","not":"\u00AC","Ntilde":"\u00D1","ntilde":"\u00F1","Oacute":"\u00D3","oacute":"\u00F3","Ocirc":"\u00D4","ocirc":"\u00F4","Ograve":"\u00D2","ograve":"\u00F2","ordf":"\u00AA","ordm":"\u00BA","Oslash":"\u00D8","oslash":"\u00F8","Otilde":"\u00D5","otilde":"\u00F5","Ouml":"\u00D6","ouml":"\u00F6","para":"\u00B6","plusmn":"\u00B1","pound":"\u00A3","quot":"\"","QUOT":"\"","raquo":"\u00BB","reg":"\u00AE","REG":"\u00AE","sect":"\u00A7","shy":"\u00AD","sup1":"\u00B9","sup2":"\u00B2","sup3":"\u00B3","szlig":"\u00DF","THORN":"\u00DE","thorn":"\u00FE","times":"\u00D7","Uacute":"\u00DA","uacute":"\u00FA","Ucirc":"\u00DB","ucirc":"\u00FB","Ugrave":"\u00D9","ugrave":"\u00F9","uml":"\u00A8","Uuml":"\u00DC","uuml":"\u00FC","Yacute":"\u00DD","yacute":"\u00FD","yen":"\u00A5","yuml":"\u00FF"} +},{}],18:[function(require,module,exports){ +module.exports={"amp":"&","apos":"'","gt":">","lt":"<","quot":"\""} + +},{}],19:[function(require,module,exports){ + +'use strict'; + + +/* eslint-disable no-bitwise */ + +var decodeCache = {}; + +function getDecodeCache(exclude) { + var i, ch, cache = decodeCache[exclude]; + if (cache) { return cache; } + + cache = decodeCache[exclude] = []; + + for (i = 0; i < 128; i++) { + ch = String.fromCharCode(i); + cache.push(ch); + } + + for (i = 0; i < exclude.length; i++) { + ch = exclude.charCodeAt(i); + cache[ch] = '%' + ('0' + ch.toString(16).toUpperCase()).slice(-2); + } + + return cache; +} + + +// Decode percent-encoded string. +// +function decode(string, exclude) { + var cache; + + if (typeof exclude !== 'string') { + exclude = decode.defaultChars; + } + + cache = getDecodeCache(exclude); + + return string.replace(/(%[a-f0-9]{2})+/gi, function(seq) { + var i, l, b1, b2, b3, b4, chr, + result = ''; + + for (i = 0, l = seq.length; i < l; i += 3) { + b1 = parseInt(seq.slice(i + 1, i + 3), 16); + + if (b1 < 0x80) { + result += cache[b1]; + continue; + } + + if ((b1 & 0xE0) === 0xC0 && (i + 3 < l)) { + // 110xxxxx 10xxxxxx + b2 = parseInt(seq.slice(i + 4, i + 6), 16); + + if ((b2 & 0xC0) === 0x80) { + chr = ((b1 << 6) & 0x7C0) | (b2 & 0x3F); + + if (chr < 0x80) { + result += '\ufffd\ufffd'; + } else { + result += String.fromCharCode(chr); + } + + i += 3; + continue; + } + } + + if ((b1 & 0xF0) === 0xE0 && (i + 6 < l)) { + // 1110xxxx 10xxxxxx 10xxxxxx + b2 = parseInt(seq.slice(i + 4, i + 6), 16); + b3 = parseInt(seq.slice(i + 7, i + 9), 16); + + if ((b2 & 0xC0) === 0x80 && (b3 & 0xC0) === 0x80) { + chr = ((b1 << 12) & 0xF000) | ((b2 << 6) & 0xFC0) | (b3 & 0x3F); + + if (chr < 0x800 || (chr >= 0xD800 && chr <= 0xDFFF)) { + result += '\ufffd\ufffd\ufffd'; + } else { + result += String.fromCharCode(chr); + } + + i += 6; + continue; + } + } + + if ((b1 & 0xF8) === 0xF0 && (i + 9 < l)) { + // 111110xx 10xxxxxx 10xxxxxx 10xxxxxx + b2 = parseInt(seq.slice(i + 4, i + 6), 16); + b3 = parseInt(seq.slice(i + 7, i + 9), 16); + b4 = parseInt(seq.slice(i + 10, i + 12), 16); + + if ((b2 & 0xC0) === 0x80 && (b3 & 0xC0) === 0x80 && (b4 & 0xC0) === 0x80) { + chr = ((b1 << 18) & 0x1C0000) | ((b2 << 12) & 0x3F000) | ((b3 << 6) & 0xFC0) | (b4 & 0x3F); + + if (chr < 0x10000 || chr > 0x10FFFF) { + result += '\ufffd\ufffd\ufffd\ufffd'; + } else { + chr -= 0x10000; + result += String.fromCharCode(0xD800 + (chr >> 10), 0xDC00 + (chr & 0x3FF)); + } + + i += 9; + continue; + } + } + + result += '\ufffd'; + } + + return result; + }); +} + + +decode.defaultChars = ';/?:@&=+$,#'; +decode.componentChars = ''; + + +module.exports = decode; + +},{}],20:[function(require,module,exports){ + +'use strict'; + + +var encodeCache = {}; + + +// Create a lookup array where anything but characters in `chars` string +// and alphanumeric chars is percent-encoded. +// +function getEncodeCache(exclude) { + var i, ch, cache = encodeCache[exclude]; + if (cache) { return cache; } + + cache = encodeCache[exclude] = []; + + for (i = 0; i < 128; i++) { + ch = String.fromCharCode(i); + + if (/^[0-9a-z]$/i.test(ch)) { + // always allow unencoded alphanumeric characters + cache.push(ch); + } else { + cache.push('%' + ('0' + i.toString(16).toUpperCase()).slice(-2)); + } + } + + for (i = 0; i < exclude.length; i++) { + cache[exclude.charCodeAt(i)] = exclude[i]; + } + + return cache; +} + + +// Encode unsafe characters with percent-encoding, skipping already +// encoded sequences. +// +// - string - string to encode +// - exclude - list of characters to ignore (in addition to a-zA-Z0-9) +// - keepEscaped - don't encode '%' in a correct escape sequence (default: true) +// +function encode(string, exclude, keepEscaped) { + var i, l, code, nextCode, cache, + result = ''; + + if (typeof exclude !== 'string') { + // encode(string, keepEscaped) + keepEscaped = exclude; + exclude = encode.defaultChars; + } + + if (typeof keepEscaped === 'undefined') { + keepEscaped = true; + } + + cache = getEncodeCache(exclude); + + for (i = 0, l = string.length; i < l; i++) { + code = string.charCodeAt(i); + + if (keepEscaped && code === 0x25 /* % */ && i + 2 < l) { + if (/^[0-9a-f]{2}$/i.test(string.slice(i + 1, i + 3))) { + result += string.slice(i, i + 3); + i += 2; + continue; + } + } + + if (code < 128) { + result += cache[code]; + continue; + } + + if (code >= 0xD800 && code <= 0xDFFF) { + if (code >= 0xD800 && code <= 0xDBFF && i + 1 < l) { + nextCode = string.charCodeAt(i + 1); + if (nextCode >= 0xDC00 && nextCode <= 0xDFFF) { + result += encodeURIComponent(string[i] + string[i + 1]); + i++; + continue; + } + } + result += '%EF%BF%BD'; + continue; + } + + result += encodeURIComponent(string[i]); + } + + return result; +} + +encode.defaultChars = ";/?:@&=+$,-_.!~*'()#"; +encode.componentChars = "-_.!~*'()"; + + +module.exports = encode; + +},{}],21:[function(require,module,exports){ +/*! http://mths.be/repeat v0.2.0 by @mathias */ +if (!String.prototype.repeat) { + (function() { + 'use strict'; // needed to support `apply`/`call` with `undefined`/`null` + var defineProperty = (function() { + // IE 8 only supports `Object.defineProperty` on DOM elements + try { + var object = {}; + var $defineProperty = Object.defineProperty; + var result = $defineProperty(object, object, object) && $defineProperty; + } catch(error) {} + return result; + }()); + var repeat = function(count) { + if (this == null) { + throw TypeError(); + } + var string = String(this); + // `ToInteger` + var n = count ? Number(count) : 0; + if (n != n) { // better `isNaN` + n = 0; + } + // Account for out-of-bounds indices + if (n < 0 || n == Infinity) { + throw RangeError(); + } + var result = ''; + while (n) { + if (n % 2 == 1) { + result += string; + } + if (n > 1) { + string += string; + } + n >>= 1; + } + return result; + }; + if (defineProperty) { + defineProperty(String.prototype, 'repeat', { + 'value': repeat, + 'configurable': true, + 'writable': true + }); + } else { + String.prototype.repeat = repeat; + } + }()); +} + +},{}]},{},[4])(4) +}); diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 000000000..9fec28a69 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,11 @@ +export enum Owner {client, server}; + +export interface Config {path?:string, runtimeOwner?: Owner, controlOwner?: Owner, port?:number, editor?: boolean, root?: string, eveRoot?: string, internal?: boolean} + +export var config:Config = {}; + +export function init(opts:Config) { + for(let key in opts) { + config[key] = opts[key]; + } +} diff --git a/src/db.ts b/src/db.ts new file mode 100644 index 000000000..e2777a103 --- /dev/null +++ b/src/db.ts @@ -0,0 +1,134 @@ +export type UUID = string; +export type EAV = [string, string, any]; +export type Record = any; + +//--------------------------------------------------------- +// Indexes +//--------------------------------------------------------- + +type IndexSubscriber = (index: T, dirty: T, self: Index) => void +class Index { + public index:T = {} as any; + public dirty:T = {} as any; + private subscribers:IndexSubscriber[] = []; + + constructor() {} + + subscribe(subscriber:IndexSubscriber) { + if(this.subscribers.indexOf(subscriber) === -1) { + this.subscribers.push(subscriber); + return true; + } + return false; + } + + unsubscribe(subscriber:IndexSubscriber) { + let ix = this.subscribers.indexOf(subscriber); + if(ix !== -1) { + this.subscribers[ix] = this.subscribers.pop()!; + return true; + } + return false; + } + + dispatchIfDirty() { + if(Object.keys(this.dirty).length === 0) return; + for(let subscriber of this.subscribers) { + subscriber(this.index, this.dirty, this); + } + } + + clearDirty() { + this.dirty = {} as any; + } + + clearIndex() { + this.index = {} as any; + } +} + +interface IndexedList{[v: string]: V[]} +export class IndexList extends Index> { + insert(key: string, value: V) { + if(!this.index[key] || this.index[key].indexOf(value) === -1) { + if(!this.index[key]) this.index[key] = []; + if(!this.dirty[key]) this.dirty[key] = []; + this.index[key].push(value); + this.dirty[key].push(value); + return true; + } + return false; + } + + remove(key: string, value: V) { + if(!this.index[key]) return false; + + let ix = this.index[key].indexOf(value) + if(ix !== -1) { + if(!this.dirty[key]) this.dirty[key] = []; + this.index[key][ix] = this.index[key].pop()!; + this.dirty[key].push(value); + return true; + } + return false; + } +}; + +interface IndexedScalar{[v: string]: V} +export class IndexScalar extends Index> { + insert(key: string, value: V) { + if(this.index[key] === undefined) { + this.index[key] = value; + this.dirty[key] = value; + return true; + } else if(this.index[key] !== value) { + throw new Error(`Unable to set multiple values on scalar index for key: '${key}' old: '${this.index[key]}' new: '${value}'`); + } + return false; + } + + remove(key: string, value: V) { + if(this.index[key] === undefined) return false; + this.dirty[key] = this.index[key]; + delete this.index[key]; + return true; + } +} + +//--------------------------------------------------------- +// DB +//--------------------------------------------------------- +type Value = string | number | boolean | UUID; + +export class DB { + protected _indexes:{[attribute:string]: IndexList} = {}; // A: V -> E + protected _records = new IndexScalar(); // E -> Record + protected _dirty = new IndexList(); // E -> A + + constructor(public id:UUID) {} + + record(entity:UUID):Record { + return this._records[entity]; + } + + index(attribute:string):IndexList { + let index = this._indexes[attribute]; + if(index) return index; + + index = new IndexList(); + this._indexes[attribute] = index; + + for(let entity in this._records.index) { + let record = this._records.index[entity]; + let values = record[attribute]; + if(!values) continue; + for(let value of values) { + index.insert(value, entity); + } + } + + return index; + } + + +} diff --git a/src/declarations.d.ts b/src/declarations.d.ts new file mode 100644 index 000000000..900294633 --- /dev/null +++ b/src/declarations.d.ts @@ -0,0 +1,115 @@ +/// +/// + +declare module "uuid"; + +declare module "microReact" { + export interface Handler { + (evt:T, elem:Element): void + } + export interface RenderHandler { + (node:HTMLElement, elem:Element): void + } + + export interface Element { + t?:string + c?:string + id?:string + parent?:string + children?:(Element|undefined)[] + ix?:number + key?:string + dirty?:boolean + semantic?:string + debug?:any + + // Content + contentEditable?:boolean + checked?:boolean + draggable?:boolean + href?:string + placeholder?:string + selected?:boolean + tabindex?:number + text?:string + type?:string + value?:string + + // Styles (Structure) + flex?:number|string + left?:number|string + top?:number|string + width?:number|string + height?:number|string + textAlign?:string + transform?:string + verticalAlign?:string + zIndex?:number + + // Styles (Aesthetic) + backgroundColor?:string + backgroundImage?:string + border?:string + borderColor?:string + borderWidth?:number|string + borderRadius?:number|string + color?:string + colspan?:number + fontFamily?:string + fontSize?:string + opacity?:number + + // Svg + svg?:boolean + x?:number|string + y?:number|string + dx?:number|string + dy?:number|string + cx?:number|string + cy?:number|string + r?:number|string + d?:number|string + fill?:string + stroke?:string + strokeWidth?:string + startOffset?:number|string + textAnchor?:string + viewBox?:string + xlinkhref?:string + + // Events + dblclick?:Handler + click?:Handler + contextmenu?:Handler + mousedown?:Handler + mousemove?:Handler + mouseup?:Handler + mouseover?:Handler + mouseout?:Handler + mouseleave?:Handler + mousewheel?:Handler + dragover?:Handler + dragstart?:Handler + dragend?:Handler + drag?:Handler + drop?:Handler + scroll?:Handler + focus?:Handler + blur?:Handler + input?:Handler + change?:Handler + keyup?:Handler + keydown?:Handler + + postRender?:RenderHandler + + [attr:string]: any + } + + class Renderer { + static compile(elem:Element):HTMLElement; + + content:HTMLElement; + render(elems:Element[]); + } +} diff --git a/src/ide.ts b/src/ide.ts new file mode 100644 index 000000000..d3811a174 --- /dev/null +++ b/src/ide.ts @@ -0,0 +1,2926 @@ +import {Renderer, Element as Elem, RenderHandler} from "microReact"; +import {Parser as MDParser} from "commonmark"; +import * as CodeMirror from "codemirror"; +import {debounce, uuid, unpad, Range, Position, isRange, compareRanges, comparePositions, samePosition, whollyEnclosed, adjustToWordBoundary} from "./util"; + +import {Span, SpanMarker, isSpanMarker, isEditorControlled, spanTypes, compareSpans, SpanChange, isSpanChange, HeadingSpan, CodeBlockSpan, DocumentCommentSpan} from "./ide/spans"; +import * as Spans from "./ide/spans"; + +import {activeElements} from "./renderer"; +import {client, indexes} from "./client"; + +//--------------------------------------------------------- +// Navigator +//--------------------------------------------------------- +/* - [x] Document Pseudo-FS + * - [x] Table of Contents + * - [x] Separate detail levels to control indentation / info overload + * - [x] 2nd priority on width + * - [x] Collapsible + * - [x] Elision (in ToC) + * - [x] Elision (in editor) + */ + +interface TreeNode { + id?: string, + name: string, + type: string, + children?: string[], + open?: boolean, + span?: Span, + + hidden?: boolean, + elisionSpan?: Span, + level?: number +} +interface TreeMap {[id:string]: TreeNode|undefined} + +class Navigator { + labels = { + folder: "Workspace", + document: "Table of Contents" + }; + open: boolean = true; + + constructor(public ide:IDE, public rootId = "root", public nodes:TreeMap = {root: {type: "folder", name: "/", children: []}}, public currentId:string = rootId) {} + + currentType():string { + let node = this.nodes[this.currentId]; + return node && node.type || "folder"; + } + + walk(rootId:string, callback:(nodeId:string, parentId?:string) => void, parentId?:string) { + let node = this.nodes[rootId]; + if(!node) return; + callback(rootId, parentId); + + if(node.children) { + for(let childId of node.children) { + this.walk(childId, callback, rootId); + } + } + } + + + loadWorkspace(id:string, name:string, files:{[filename:string]: string}, parentId = this.rootId) { + let root:TreeNode = this.nodes[id] = {id, name, type: "folder", open: true}; + + let parent = root; + for(let curId in files) { + let node:TreeNode = {id: curId, name: curId.split("/").pop(), type: "document"}; + this.nodes[curId] = node; + if(!parent.children) parent.children = [curId]; + else parent.children.push(curId); + } + + if(id !== this.rootId) { + parent = this.nodes[parentId]; + if(!parent) throw new Error(`Unable to load document into non-existent folder ${parentId}`); + if(!parent.children) parent.children = []; + if(parent.children.indexOf(id) === -1) { + parent.children.push(id); + } + } + } + + loadDocument(id:string, name:string) { + let editor = this.ide.editor; + let doc = editor.cm.getDoc(); + let headings = editor.getAllSpans("heading") as HeadingSpan[]; + headings.sort(compareSpans); + + let root:TreeNode = this.nodes[id]; + if(!root) { + console.error("Cannot load non-existent document."); + return; + } + root.open = true; + root.children = undefined; + + let stack:TreeNode[] = [root]; + for(let heading of headings) { + let curId = heading.id; + let loc = heading.find(); + if(!loc) continue; + + while((stack.length > 1) && heading.source.level <= stack[stack.length - 1].level) stack.pop(); + let parent = stack[stack.length - 1]; + if(!parent.children) parent.children = [curId]; + else parent.children.push(curId); + + let old = this.nodes[curId]; + let node:TreeNode = {id: curId, name: doc.getLine(loc.from.line), type: "section", level: heading.source.level, span: heading, open: old ? old.open : true, hidden: old ? old.hidden : false, elisionSpan: old ? old.elisionSpan : undefined}; + stack.push(node); + this.nodes[curId] = node; + } + + this.nodes[id] = root; + } + + updateNode(span:HeadingSpan) { + if(this.currentType() !== "document") return; + + let nodeId = span.id; + let node = this.nodes[nodeId]; + + let loc = span.find(); + if(node && !loc) { + if(node.elisionSpan) node.elisionSpan.clear(); + this.nodes[nodeId] = undefined; + + } else if(node) { + node.hidden = span.isHidden(); + + } else if(!node && loc) { + let cur = loc.from; + let parentId:string; + let siblingId:string|undefined; + do { + let parentSpan = this.ide.editor.findHeadingAt(cur); + let parentLoc = parentSpan && parentSpan.find(); + cur = parentLoc ? parentLoc.from : {line: 0, ch: 0}; + siblingId = parentId; + parentId = parentSpan ? parentSpan.id : this.currentId; + + } while(parentId !== this.currentId && this.nodes[parentId]!.level >= span.source.level); + + let parentNode = this.nodes[parentId]!; + if(!parentNode.children) parentNode.children = [nodeId]; + else { + let ix = parentNode.children.length; + if(siblingId) { + ix = parentNode.children.indexOf(siblingId); + ix = (ix === -1) ? parentNode.children.length : ix; + } + parentNode.children.splice(ix, 0, nodeId); + } + let doc = this.ide.editor.cm.getDoc(); + this.nodes[nodeId] = {id: nodeId, name: doc.getLine(loc.from.line), type: "section", level: span.source.level, span, open: true, hidden: span.isHidden()}; + } + } + + updateElision() { + let sections:{nodeId: string, hidden: boolean, range:Range}[] = []; + + for(let nodeId in this.nodes) { + let node = this.nodes[nodeId]; + if(!node || node.type !== "section") continue; + + let heading = node.span as HeadingSpan; + let range = heading.getSectionRange(); + sections.push({nodeId, hidden: node.hidden, range}); + } + + if(!sections.length) { + // Only one source can be safely eliding at any given time. + for(let span of this.ide.editor.getAllSpans("elision")) { + span.clear(); + } + return; + } + + sections.sort((a, b) => { + let fromDir = comparePositions(a.range.from, b.range.from); + if(fromDir) return fromDir; + return comparePositions(a.range.to, b.range.to); + }); + + let visibleRanges:Range[] = []; + let currentRange:Range|undefined; + for(let section of sections) { + if(!section.hidden) { + if(!currentRange) currentRange = {from: section.range.from, to: section.range.to}; + else currentRange.to = section.range.to; + + } else { + if(currentRange) { + if(comparePositions(section.range.from, currentRange.to) < 0) { + currentRange.to = section.range.from; + } + visibleRanges.push(currentRange); + currentRange = undefined; + } + } + } + + if(currentRange) { + visibleRanges.push(currentRange); + } + + let editor = this.ide.editor; + let doc = editor.cm.getDoc(); + // Capture the current topmost un-elided line in the viewport. We'll use this to maintain your scroll state (to some extent) when elisions are nuked. + // Only one source can be safely eliding at any given time. + let topVisible:number|undefined; + for(let span of editor.getAllSpans("elision")) { + let loc = span.find(); + if(loc && (!topVisible || loc.to.line < topVisible)) { + topVisible = loc.to.line; + } + span.clear(); + } + + if(visibleRanges.length) { + editor.markBetween(visibleRanges, {type: "elision"}); + } else { + editor.markSpan({line: 0, ch: 0}, {line: doc.lineCount(), ch: 0}, {type: "elision"}); + } + + if(visibleRanges.length === 1 && topVisible) { + let firstRange = visibleRanges[0]; + if(firstRange.from.line === 0 && firstRange.to.line >= doc.lastLine()) { + editor.scrollToPosition({line: topVisible + 1, ch: 0}); + } + } + } + + isFocused() { + return this.ide.editor.getAllSpans("elision").length; + } + + // Event Handlers + togglePane = (event:MouseEvent, elem) => { + this.open = !this.open; + this.ide.render(); + event.stopPropagation(); + } + + navigate = (event, elem:{nodeId:string}) => { + this.currentId = elem.nodeId || this.rootId; + let node = this.nodes[elem.nodeId]; + if(node && node.type === "document") { + this.ide.loadFile(elem.nodeId); + } + this.ide.render(); + } + + toggleBranch = (event:MouseEvent, {nodeId}) => { + let node = this.nodes[nodeId]; + if(!node) return; + node.open = !node.open; + this.ide.render(); + event.stopPropagation(); + } + + gotoSpan = (event:MouseEvent, {nodeId}) => { + let node = this.nodes[nodeId]; + if(!node) return; + let loc = node.span.find(); + if(!loc) return; + if(node.span.constructor === HeadingSpan) { + let heading = node.span as HeadingSpan; + loc = heading.getSectionRange() || loc; + } + this.ide.editor.cm.scrollIntoView(loc, 20); + } + + _inheritParentElision = (nodeId: string, parentId?: string) => { + let node = this.nodes[nodeId]; + let parent = this.nodes[parentId]; + if(!node || !parent) return; + node.hidden = parent.hidden; + } + + toggleElision = (event, {nodeId}) => { + let node = this.nodes[nodeId]; + if(!node) return; + this.ide.editor.cm.operation( () => { + node.hidden = !node.hidden; + this.walk(nodeId, this._inheritParentElision); + this.updateElision(); + }); + + this.ide.render(); + event.stopPropagation(); + } + + toggleInspectorFocus = () => { + if(this.isFocused()) { + client.sendEvent([{tag: ["inspector", "unfocus-current"]}]); + for(let nodeId in this.nodes) { + let node = this.nodes[nodeId]; + if(!node) continue; + if(node.hidden) node.hidden = false; + } + this.updateElision(); + } else { + client.sendEvent([{tag: ["inspector", "focus-current"]}]); + } + } + + createDocument = (event:MouseEvent, {nodeId}) => { + // @FIXME: This needs to be keyed off nodeId, not name for multi-level workspaces. + // Top level node id is currently hardwired for what I imagine seemed like a good reason at the time. + let node = this.nodes[nodeId]; + if(!node) return; + this.ide.createDocument(node.name); + node.name + } + + // Elements + workspaceItem(nodeId:string):Elem { + let node = this.nodes[nodeId]; + if(!node) return {c: "tree-item", nodeId}; + + let subtree:Elem|undefined; + if(node.type === "folder") { + let items:(Elem|undefined)[] = []; + if(node.open) { + for(let childId of node.children || []) { + items.push(this.workspaceItem(childId)); + } + } + subtree = {c: "tree-items", children: items}; + } + + return {c: `tree-item ${subtree ? "branch" : "leaf"} ${node.type} ${subtree && !node.open ? "collapsed" : ""}`, nodeId, children: [ + {c: "flex-row", children: [ + {c: `label ${subtree ? "ion-ios-arrow-down" : "no-icon"}`, text: node.name, nodeId, click: subtree ? this.toggleBranch : this.navigate}, // icon should be :before + {c: "controls", children: [ + subtree ? {c: "new-btn ion-ios-plus-empty", nodeId, click: this.createDocument} : undefined, + {c: "delete-btn ion-ios-close-empty", click: () => console.log("delete folder or document w/ confirmation")} + ]} + ]}, + subtree + ]}; + } + + tocItem(nodeId:string):Elem { + let node = this.nodes[nodeId]; + if(!node) return {c: "tree-item", nodeId}; + + let subtree:Elem|undefined; + if(node.children) { + let items:(Elem|undefined)[] = []; + if(node.open) { + for(let childId of node.children) { + items.push(this.tocItem(childId)); + } + } + subtree = {c: "tree-items", children: items}; + } + + if(node.type === "document") { + return {c: `tree-item ${nodeId === this.rootId ? "root" : ""} ${node.type}`, nodeId, children: [ + subtree + ]}; + } + + return {c: `tree-item ${subtree ? "branch" : "leaf"} ${nodeId === this.rootId ? "root" : ""} ${node.type} item-level-${node.level} ${subtree && !node.open ? "collapsed" : ""} ${node.hidden ? "hidden" : ""}`, nodeId, children: [ + {c: "flex-row", children: [ + {c: `label ${subtree && !node.level ? "ion-ios-arrow-down" : "no-icon"}`, text: node.name, nodeId, click: node.span ? this.gotoSpan : undefined}, // icon should be :before + {c: "controls", children: [ + {c: `elide-btn ${node.hidden ? "ion-android-checkbox-outline-blank" : "ion-android-checkbox-outline"}`, nodeId, click: this.toggleElision}, + ]} + ]}, + subtree + ]}; + } + + inspectorControls():Elem { + return {c: "inspector-controls", children: [ + {t: "button", c: "inspector-hide", text: this.isFocused() ? "Show all" : "Filter to selected", click: this.toggleInspectorFocus} + ]}; + } + + header():Elem { + let type = this.currentType(); + return {c: "navigator-header", children: [ + {c: "controls", children: [ + this.open ? {c: `up-btn flex-row`, click: this.navigate, children: [ + {c: `up-btn ion-android-arrow-up ${(type === "folder") ? "disabled" : ""}`}, + {c: "label", text: "examples"}, + ]} : undefined, + {c: "flex-spacer"}, + {c: `${this.open ? "expand-btn" : "collapse-btn"} ion-ios-arrow-back`, click: this.togglePane}, + ]}, + this.ide.inspecting ? this.inspectorControls() : {c: "inspector-controls"}, + ]}; + } + + render():Elem { + let nodeId = this.currentId; + let root = this.nodes[nodeId]; + if(!root) return {c: "navigator-pane", children: [ + {c: "navigator-pane-inner", children: [ + this.header(), + {c: "new-btn ion-ios-plus-empty", click: () => console.log("new folder or document")} + ]} + ]}; + + let tree:Elem|undefined; + if(root.type === "folder") { + tree = this.workspaceItem(nodeId); + } else if(root.type === "document") { + tree = this.tocItem(nodeId); + } + return {c: `navigator-pane ${this.open ? "" : "collapsed"}`, click: this.open ? undefined : this.togglePane, children: [ + {c: "navigator-pane-inner", children: [ + this.header(), + tree + ]} + ]}; + } +} + +//--------------------------------------------------------- +// Editor +//--------------------------------------------------------- +/* - [x] Exactly 700px + * - [x] Markdown styling + * - [x] Add missing span types + * - [x] Event handlers e.g. onChange, etc. + * - [x] Get spans updating again + * - [x] BUG: Formatting selected too inclusive: |A*A|A* -Cmd-Bg-> AAA + * - [x] Syntax highlighting + * - [x] Live parsing + * - [x] Undo + * - [ ] Display cardinality badges + * - [ ] Show related (at least action -> EAV / EAV -> DOM + * - [ ] Autocomplete (at least language constructs, preferably also expression schemas and known tags/names/attributes) + */ + +type FormatType = "strong"|"emph"|"code"|"code_block"; +type FormatLineType = "heading"|"item"|"elision"; +type FormatAction = "add"|"remove"|"split"; + +interface EditorNode extends HTMLElement { cm?: CodeMirror.Editor } +type MDSpan = [number, number, commonmark.Node]; + +let _mdParser = new MDParser(); +function parseMarkdown(input:string):{text: string, spans: MDSpan[]} { + let parsed = _mdParser.parse(input); + let walker = parsed.walker(); + var cur:commonmark.NodeWalkingStep; + var text:string[] = []; + var pos = 0; + var lastLine = 1; + var spans:MDSpan[] = []; + var context:{node: commonmark.Node, start: number}[] = []; + while(cur = walker.next()) { + let node = cur.node; + if(cur.entering) { + while(node.sourcepos && node.sourcepos[0][0] > lastLine) { + lastLine++; + pos++; + text.push("\n"); + } + if(node.type !== "text") { + context.push({node, start: pos}); + } + if(node.type == "text" || node.type == "code_block" || node.type == "code") { + text.push(node.literal); + pos += node.literal.length; + } + if(node.type == "softbreak") { + text.push("\n"); + pos += 1; + lastLine++; + } + if(node.type == "code_block") { + let start = context[context.length - 1].start; + spans.push([start, pos, node]); + lastLine = node.sourcepos[1][0] + 1; + } + if(node.type == "code") { + let start = context[context.length - 1].start; + spans.push([start, pos, node]); + } + } else { + let info = context.pop(); + if(!info) throw new Error("Invalid context stack while parsing markdown"); + if(node.type == "emph" || node.type == "strong" || node.type == "link") { + spans.push([info.start, pos, node]); + } else if(node.type == "heading" || node.type == "item") { + spans.push([info.start, info.start, node]); + } + } + } + return {text: text.join(""), spans}; +} + +export class Change implements CodeMirror.EditorChange { + type:string = "range"; + + constructor(protected _raw:CodeMirror.EditorChange) {} + + /** String representing the origin of the change event and whether it can be merged with history. */ + get origin() { return this._raw.origin; } + /** Lines of text that used to be between from and to, which is overwritten by this change. */ + get text() { return this._raw.text; } + /** Lines of text that used to be between from and to, which is overwritten by this change. */ + get removed() { return this._raw.removed; } + /** Position (in the pre-change coordinate system) where the change started. */ + get from() { return this._raw.from; } + /** Position (in the pre-change coordinate system) where the change ended. */ + get to() { return this._raw.to; } + /** Position (in the post-change coordinate system) where the change eneded. */ + get final() { + let {from, to, text} = this; + let final = {line: from.line + (text.length - 1), ch: text[text.length - 1].length}; + if(text.length == 1) { + final.ch += from.ch; + } + return final; + } + + /** String of all text added in the change. */ + get addedText() { return this.text.join("\n"); } + /** String of all text removed in the change. */ + get removedText() { return this.removed.join("\n"); } + + /** Whether this change just a single enter. */ + isNewlineChange() { + return this.text.length == 2 && this.text[1] == ""; + } + + /** Inverts a change for undo. */ + invert() { return new ChangeInverted(this._raw) as Change; } +} + +class ChangeLinkedList extends Change { + constructor(protected _raw:CodeMirror.EditorChangeLinkedList) { + super(_raw); + } + + /** Next change object in sequence, if any. */ + next() { + return this._raw.next && new ChangeLinkedList(this._raw.next); + } +} +function isRangeChange(x:Change|SpanChange): x is Change { + return x && x.type === "range"; +} + + +export class ChangeCancellable extends Change { + constructor(protected _raw:CodeMirror.EditorChangeCancellable) { + super(_raw); + } + + get canceled() { return this._raw.canceled; } + + update(from?:Position, to?:Position, text?:string) { + return this._raw.update(from, to, text); + } + + cancel() { + return this._raw.cancel(); + } +} + +class ChangeInverted extends Change { + /** Lines of text that used to be between from and to, which is overwritten by this change. */ + get text() { return this._raw.removed; } + /** Lines of text that used to be between from and to, which is overwritten by this change. */ + get removed() { return this._raw.text; } + /** Inverts a change for undo. */ + invert() { return new Change(this._raw); } +} + +function formattingChange(span:Span, change:Change, action?:FormatAction) { + let editor = span.editor; + let loc = span.find(); + if(!loc) return; + // Cut the changed range out of a span + if(action == "split") { + let final = change.final; + editor.markSpan(loc.from, change.from, span.source); + // If the change is within the right edge of the span, recreate the remaining segment + if(comparePositions(final, loc.to) === -1) { + editor.markSpan(final, loc.to, span.source); + } + span.clear(); + + } else if(!action) { + // If we're at the end of the span, expand it to include the change + if(samePosition(loc.to, change.from)) { + span.clear(); + editor.markSpan(loc.from, change.final, span.source); + } + } +} + +function ctrlify(keymap) { + let finalKeymap = {}; + for(let key in keymap) { + finalKeymap[key] = keymap[key]; + if(key.indexOf("Cmd") > -1) { + finalKeymap[key.replace("Cmd", "Ctrl")] = keymap[key]; + } + } + return finalKeymap; +} + +// Register static commands +let _rawUndo = CodeMirror.commands["undo"]; +CodeMirror.commands["undo"] = function(cm:CMEditor) { + if(!cm.editor) _rawUndo.apply(this, arguments); + else cm.editor.undo(); +} +let _rawRedo = CodeMirror.commands["redo"]; +CodeMirror.commands["redo"] = function(cm:CMEditor) { + if(!cm.editor) _rawRedo.apply(this, arguments); + else cm.editor.redo(); +} + +function debugTokenWithContext(text:string, start:number, end:number):string { + let lineStart = text.lastIndexOf("\n", start) + 1; + let lineEnd = text.indexOf("\n", end); + if(lineEnd === -1) lineEnd = undefined; + let tokenStart = start - lineStart; + let tokenEnd = end - lineStart; + let line = text.substring(lineStart, lineEnd); + + return line.substring(0, tokenStart) + "|" + line.substring(tokenStart, tokenEnd) + "|" + line.substring(tokenEnd); +} + +interface HistoryItem { finalized?: boolean, changes:(Change|SpanChange)[] } +interface CMEditor extends CodeMirror.Editor { + editor?:Editor +} +export class Editor { + defaults:CodeMirror.EditorConfiguration = { + scrollPastEnd: true, + scrollbarStyle: "simple", + tabSize: 2, + lineWrapping: true, + lineNumbers: false, + extraKeys: ctrlify({ + "Cmd-Enter": () => this.ide.eval(true), + "Shift-Cmd-Enter": () => this.ide.eval(false), + "Alt-Enter": () => this.ide.tokenInfo(), + "Cmd-B": () => this.format({type: "strong"}), + "Cmd-I": () => this.format({type: "emph"}), + "Cmd-Y": () => this.format({type: "code"}), + "Cmd-K": () => this.format({type: "code_block"}), + "Cmd-1": () => this.format({type: "heading", level: 1}), + "Cmd-2": () => this.format({type: "heading", level: 2}), + "Cmd-3": () => this.format({type: "heading", level: 3}), + "Cmd-L": () => this.format({type: "item"}) + }) + }; + + cm:CMEditor; + + /** Whether the editor has changed since the last update. */ + dirty = false; + + /** Whether the editor is being externally updated with new content. */ + reloading = false; + + /** Formatting state for the editor at the cursor. */ + formatting:{[formatType:string]: FormatAction} = {}; + + /** Whether the editor is currently processing CM change events */ + changing = false; + /** Cache of the spans affected by the current set of changes */ + changingSpans?:Span[]; + /** Cache of spans currently in a denormalized state. So long as this is non-empty, the editor may not sync with the language service. */ + denormalizedSpans:Span[] = []; + + /** Undo history state */ + history:{position:number, transitioning:boolean, items: HistoryItem[]} = {position: 0, items: [], transitioning: false}; + + /** Whether to show the new block button at the cursor. */ + protected showNewBlockBar = false; + protected newBlockBar:EditorBarElem; + + /** Whether to show the format bar at the cursor. */ + protected showFormatBar = false; + + constructor(public ide:IDE) { + this.cm = CodeMirror(() => undefined, this.defaults); + this.cm.editor = this; + this.cm.on("beforeChange", (editor, rawChange) => this.onBeforeChange(rawChange)); + this.cm.on("change", (editor, rawChange) => this.onChange(rawChange)); + this.cm.on("changes", (editor, rawChanges) => this.onChanges(rawChanges)); + this.cm.on("cursorActivity", this.onCursorActivity); + this.cm.on("scroll", this.onScroll); + + this.newBlockBar = {editor: this, active: false}; + } + + reset() { + this.history.position = 0; + this.history.items = []; + this.history.transitioning = true; + this.reloading = true; + this.cm.setValue(""); + for(let span of this.getAllSpans()) { + span.clear(); + } + this.reloading = false; + this.history.transitioning = false; + } + + // This is a new document and we need to rebuild it from scratch. + loadDocument(id:string, text:string, packed:any[], attributes:{[id:string]: any|undefined}) { + // Reset history and suppress storing the load as a history step. + this.history.position = 0; + this.history.items = []; + this.history.transitioning = true; + + if(packed.length % 4 !== 0) throw new Error("Invalid span packing, unable to load."); + this.cm.operation(() => { + this.reloading = true; + + // this is a new document and we need to rebuild it from scratch. + this.cm.setValue(text); + let doc = this.cm.getDoc(); + + for(let i = 0; i < packed.length; i += 4) { + let from = doc.posFromIndex(packed[i]); + let to = doc.posFromIndex(packed[i + 1]); + let type = packed[i + 2]; + let id = packed[i + 3]; + + //console.info(type, debugTokenWithContext(text, packed[i], packed[i + 1])); + + let source = attributes[id] || {}; + source.type = type; + source.id = id; + this.markSpan(from, to, source); + } + }); + this.reloading = false; + this.history.transitioning = false; + this.dirty = false; + } + + // This is an update to an existing document, so we need to figure out what got added and removed. + updateDocument(packed:any[], attributes:{[id:string]: any|undefined}) { + if(packed.length % 4 !== 0) throw new Error("Invalid span packing, unable to load."); + + let addedDebug = []; + let removedDebug = []; + + this.cm.operation(() => { + this.reloading = true; + let doc = this.cm.getDoc(); + + let cursorLine = doc.getCursor().line; + + // Find all runtime-controlled spans (e.g. syntax highlighting, errors) that are unchanged and mark them as such. + // Unmarked spans will be swept afterwards. + // Set editor-controlled spans aside. We'll match them up to maintain id stability afterwards + let controlledOffsets = {}; + let touchedIds = {}; + for(let i = 0; i < packed.length; i += 4) { + // if(isEditorControlled(packed[i + 2])) + // console.info(packed[i + 2], debugTokenWithContext(doc.getValue(), packed[i], packed[i + 1])); + + + let start = packed[i]; + let type = packed[i + 2]; + if(isEditorControlled(type)) { + if(!controlledOffsets[type]) controlledOffsets[type] = [i]; + else controlledOffsets[type].push(i); + } else { + let from = doc.posFromIndex(packed[i]); + let to = doc.posFromIndex(packed[i + 1]); + let type = packed[i + 2]; + let id = packed[i + 3]; + + let source = attributes[id] || {}; + source.type = type; + source.id = id; + if(type === "document_comment") { + source.delay = 1000; + } + + let spans = this.findSpansAt(from, type); + let unchanged = false; + for(let span of spans) { + let loc = span.find(); + if(loc && samePosition(to, loc.to) && span.sourceEquals(source)) { + span.source = source; + if(span.refresh) span.refresh(); + if(type === "document_comment") { + (span as any).updateWidget(); + } + touchedIds[span.id] = true; + unchanged = true; + break; + } + } + + if(!unchanged) { + let span = this.markSpan(from, to, source); + touchedIds[span.id] = true; + addedDebug.push(span); + } + } + } + + for(let type in controlledOffsets) { + let offsets = controlledOffsets[type]; + let spans = this.getAllSpans(type); + if(offsets.length !== spans.length) { + throw new Error(`The runtime may not add, remove, or move editor controlled spans of type '${type}'. Expected ${spans.length} got ${offsets.length}`); + } + spans.sort(compareSpans); + + for(let spanIx = 0; spanIx < spans.length; spanIx++) { + let span = spans[spanIx]; + let offset = offsets[spanIx]; + + let id = packed[offset + 3]; + span.source.id = id; + } + } + + // Nuke untouched spans + for(let span of this.getAllSpans()) { + if(span.isEditorControlled()) continue; // If the span is editor controlled, it's not our business. + if(touchedIds[span.id]) continue; // If the span was added or updated, leave it be. + removedDebug.push(span); + span.clear(); + } + }); + + //console.log("updated:", this.getAllSpans().length, "added:", addedDebug, "removed:", removedDebug); + this.reloading = false; + } + + // This is an update to an existing document, so we need to figure out what got added and removed. + injectSpans(packed:any[], attributes:{[id:string]: any|undefined}) { + if(packed.length % 4 !== 0) throw new Error("Invalid span packing, unable to load."); + + this.cm.operation(() => { + this.reloading = true; + let doc = this.cm.getDoc(); + + let controlledOffsets = {}; + let touchedIds = {}; + for(let i = 0; i < packed.length; i += 4) { + if(isEditorControlled(packed[i + 2])) + console.info(packed[i + 2], debugTokenWithContext(doc.getValue(), packed[i], packed[i + 1])); + + let start = packed[i]; + let type = packed[i + 2]; + if(isEditorControlled(type)) { + throw new Error(`The parser may not inject editor controlled spans of type '${type}'`); + } else { + let from = doc.posFromIndex(packed[i]); + let to = doc.posFromIndex(packed[i + 1]); + let type = packed[i + 2]; + let id = packed[i + 3]; + + let source = attributes[id] || {}; + source.type = type; + source.id = id; + + let spans = this.findSpansAt(from, type); + let unchanged = false; + for(let span of spans) { + let loc = span.find(); + if(loc && samePosition(to, loc.to) && span.sourceEquals(source)) { + span.source = source; + if(span.refresh) span.refresh(); + unchanged = true; + break; + } + } + + if(!unchanged) { + let span = this.markSpan(from, to, source); + } + } + } + }); + + this.reloading = false; + } + + toMarkdown() { + let cm = this.cm; + let doc = cm.getDoc(); + let spans = this.getAllSpans(); + let fullText = cm.getValue(); + let markers:{pos: number, start?:boolean, isBlock?:boolean, isLine?:boolean, source:any, span?:Span}[] = []; + for(let span of spans) { + let loc = span.find(); + if(!loc) continue; + markers.push({pos: doc.indexFromPos(loc.from), start: true, isBlock: span.isBlock(), isLine: span.isLine(), source: span.source, span}); + markers.push({pos: doc.indexFromPos(loc.to), start: false, isBlock: span.isBlock(), isLine: span.isLine(), source: span.source, span}); + } + markers.sort((a, b) => { + let delta = a.pos - b.pos; + if(delta !== 0) return delta; + if(a.isBlock && !b.isBlock) return -1; + if(b.isBlock && !a.isBlock) return 1; + if(a.isLine && !b.isLine) return -1; + if(b.isLine && !a.isLine) return 1; + if(a.start && !b.start) return 1; + if(b.start && !a.start) return -1; + if(a.source.type === b.source.type) return 0; + else if(a.source.type === "link") return a.start ? 1 : -1; + else if(b.source.type === "link") return b.start ? -1 : 1; + return 0; + }); + + let pos = 0; + let pieces:string[] = []; + for(let mark of markers) { + if(!mark.source) continue; + + // If the cursor isn't at this mark yet, push the range between and advance the cursor. + if(pos !== mark.pos) { + pieces.push(fullText.substring(pos, mark.pos)); + pos = mark.pos; + } + + // Break each known span type out into its markdown equivalent. + let type = mark.source.type; + if(type === "heading" && mark.start) { + for(let ix = 0; ix < mark.source.level; ix++) { + pieces.push("#"); + } + pieces.push(" "); + } else if(type == "link" && !mark.start) { + pieces.push(`](${mark.source.destination})`); + } else if(type === "emph") { + pieces.push("*"); + } else if(type == "strong") { + pieces.push("**"); + } else if(type == "code") { + pieces.push("`"); + } else if(type == "code_block" && mark.start) { + pieces.push("```" + (mark.source.info || "") + "\n"); + + } else if(type == "code_block" && !mark.start) { + // if the last character of the block is not a \n, we need to + // add one since the closing fence must be on its own line. + let last = pieces[pieces.length - 1]; + if(last[last.length - 1] !== "\n") { + pieces.push("\n"); + } + pieces.push("```\n"); + } else if(type == "item" && mark.start && mark.source.listData.type == "bullet") { + pieces.push("- "); + } else if(type == "item" && mark.start && mark.source.listData.type == "ordered") { + pieces.push(`${mark.source.listData.start}. `); + } else if(type == "link" && mark.start) { + pieces.push("["); + } + } + + // If there's any text after all the markers have been processed, glom that on. + if(pos < fullText.length) { + pieces.push(fullText.substring(pos)); + } + + return pieces.join(""); + } + + refresh() { + this.cm.refresh(); + } + + queueUpdate = debounce((shouldEval = false) => { + if(!this.reloading && this.denormalizedSpans.length === 0) this.ide.queueUpdate(shouldEval); + }, 0); + + jumpTo(id:string) { + for(let span of this.getAllSpans()) { + if(span.source.id === id) { + let loc = span.find(); + if(!loc) break; + this.cm.scrollIntoView(loc, 20); + break; + } + } + } + + scrollToPosition(position:Position) { + let top = this.cm.cursorCoords(position, "local").top; + this.cm.scrollTo(0, Math.max(top - 100, 0)); + } + + //------------------------------------------------------- + // Spans + //------------------------------------------------------- + + getSpanBySourceId(id:string):Span|undefined { + for(let span of this.getAllSpans()) { + if(span.source.id === id) return span; + } + } + + getAllSpans(type?:string):Span[] { + let doc = this.cm.getDoc(); + let marks:SpanMarker[] = doc.getAllMarks(); + let spans:Span[] = []; + for(let mark of marks) { + if(mark.span && (!type || mark.span.source.type === type)) { + spans.push(mark.span); + } + } + return spans; + } + + findSpans(start:Position, stop:Position, type?:string):Span[] { + let doc = this.cm.getDoc(); + let marks:SpanMarker[] = doc.findMarks(start, stop); + let spans:Span[] = []; + for(let mark of marks) { + if(mark.span && (!type || mark.span.source.type === type)) { + spans.push(mark.span); + } + } + return spans; + } + + findSpansAt(pos:Position, type?:string):Span[] { + let doc = this.cm.getDoc(); + let marks:SpanMarker[] = doc.findMarksAt(pos); + let spans:Span[] = []; + for(let mark of marks) { + if(mark.span && (!type || mark.span.source.type === type)) { + spans.push(mark.span); + } + } + return spans; + } + + /** Create a new Span representing the given source in the document. */ + markSpan(from:Position, to:Position, source:any) { + let SpanClass:(typeof Span) = spanTypes[source.type] || spanTypes["default"]; + let span = new SpanClass(this, from, to, source); + return span; + } + + /** Create new Spans wrapping the text between each given span id or range. */ + markBetween(idsOrRanges:(string[]|Range[]), source:any, bounds?:Range): Span[] { + return this.cm.operation(() => { + if(!idsOrRanges.length) return []; + let ranges:Range[]; + + if(typeof idsOrRanges[0] === "string") { + let ids:string[] = idsOrRanges as string[]; + ranges = []; + let spans:Span[]; + if(bounds) { + spans = this.findSpansAt(bounds.from).concat(this.findSpans(bounds.from, bounds.to)); + } else { + spans = this.getAllSpans(); + } + for(let span of spans) { + if(ids.indexOf(span.source.id) !== -1) { + let loc = span.find(); + if(!loc) continue; + if(span.isLine()) { + loc = {from: loc.from, to: {line: loc.from.line + 1, ch: 0}}; + } + ranges.push(loc); + } + } + } else { + ranges = idsOrRanges as Range[]; + } + + if(!ranges.length) return; + + let doc = this.cm.getDoc(); + ranges.sort(compareRanges); + + let createdSpans:Span[] = []; + + let start = bounds && bounds.from || {line: 0, ch: 0}; + for(let range of ranges) { + let from = doc.posFromIndex(doc.indexFromPos(range.from) - 1); + if(comparePositions(start, from) < 0) { + createdSpans.push(this.markSpan(start, {line: from.line, ch: 0}, source)); + } + + start = doc.posFromIndex(doc.indexFromPos(range.to) + 1); + } + + let last = ranges[ranges.length - 1]; + let to = doc.posFromIndex(doc.indexFromPos(last.to) + 1); + let end = bounds && bounds.to || doc.posFromIndex(doc.getValue().length); + if(comparePositions(to, end) < 0) { + createdSpans.push(this.markSpan(to, end, source)); + } + + for(let range of ranges) { + for(let span of this.findSpans(range.from, range.to)) { + span.unhide(); + if(span.refresh) span.refresh(); + + } + } + this.queueUpdate(); + return createdSpans; + }); + } + + clearSpans(type: string, bounds?:Range) { + this.cm.operation(() => { + let spans:Span[]; + if(bounds) spans = this.findSpans(bounds.from, bounds.to, type); + else spans = this.getAllSpans(type); + + for(let span of spans) { + span.clear(); + } + }); + } + + findHeadingAt(pos:Position):HeadingSpan|undefined { + let from = {line: 0, ch: 0}; + let headings = this.findSpans(from, pos, "heading") as HeadingSpan[]; + if(!headings.length) return undefined; + + headings.sort(compareSpans); + let next = headings[headings.length - 1]; + return next; + } + + //------------------------------------------------------- + // Formatting + //------------------------------------------------------- + + inCodeBlock(pos:Position) { + let inCodeBlock = false; + for(let span of this.getAllSpans("code_block")) { + let loc = span.find(); + if(!loc) continue; + if(loc.from.line <= pos.line && comparePositions(loc.to, pos) > 0) { + return true; + } + } + } + + /** Create a new span representing the given source, collapsing and splitting existing spans as required to maintain invariants. */ + formatSpan(from:Position, to:Position, source:any):Span[] { + let selection = {from, to}; + + let spans = this.findSpans(from, to, source.type); + let formatted = false; + let neue:Span[] = []; + for(let span of spans) { + let loc = span.find(); + if(!loc) continue; + + // If the formatted range matches an existing span of the same type, clear it. + if(samePosition(loc.from, from) && samePosition(loc.to, to)) { + span.clear(); + formatted = true; + + // If formatted range wholly encloses a span of the same type, clear it. + } else if(whollyEnclosed(loc, selection)) { + span.clear(); + + // If the formatted range is wholly enclosed in a span of the same type, split the span around it. + } else if(whollyEnclosed(selection, loc)) { + if(!samePosition(loc.from, from)) neue.push(this.markSpan(loc.from, from, source)); + if(!samePosition(to, loc.to)) neue.push(this.markSpan(to, loc.to, source)); + span.clear(); + formatted = true; + + // If the formatted range intersects the end of a span of the same type, clear the intersection. + } else if(comparePositions(loc.to, from) > 0) { + neue.push(this.markSpan(loc.from, from, source)); + span.clear(); + + // If the formatted range intersects the start of a span of the same type, clear the intersection. + } else if(comparePositions(loc.from, to) < 0) { + neue.push(this.markSpan(to, loc.to, source)); + span.clear(); + } + } + + // If we haven't already formatted by removing existing span(s) then we should create a new span + if(!formatted) { + neue.push(this.markSpan(from, to, source)); + } + + for(let span of neue) { + this.trackDenormalized(span); + } + + return neue; + } + + format(source:{type:string, level?: number, listData?: {type:"ordered"|"bullet", start?: number}}, refocus = false) { + let SpanClass:(typeof Span) = spanTypes[source.type] || spanTypes["default"]; + + let style = SpanClass.style(); + if(style === "inline") { + this.formatInline(source); + + } else if(style === "line") { + this.formatLine(source); + + } else if(style === "block") { + this.formatBlock(source); + } + + if(refocus) this.cm.focus(); + this.newBlockBar.active = false; + + this.queueUpdate(); + } + + formatInline(source:{type:string}) { + this.finalizeLastHistoryEntry(); + let doc = this.cm.getDoc(); + this.cm.operation(() => { + let from = doc.getCursor("from"); + from = {line: from.line, ch: adjustToWordBoundary(from.ch, doc.getLine(from.line), "left")}; + + // If we have a selection, format it, expanded to the nearest word boundaries. + // Or, if we're currently in a word, format the word. + if(doc.somethingSelected() || from.ch !== doc.getCursor("from").ch) { + let to = doc.getCursor("to"); + to = {line: to.line, ch: adjustToWordBoundary(to.ch, doc.getLine(to.line), "right")}; + + // No editor-controlled span may be created within a codeblock. + // @NOTE: This feels like a minor layor violation. + if(from.line !== to.line && this.findSpans(from, to, "code_block").length || this.findSpansAt(from, "code_block").length) return; + + this.formatSpan(from, to, source) + + // Otherwise we want to change our current formatting state. + } else { + let action:FormatAction = "add"; // By default, we just want our following changes to be bold + let cursor = doc.getCursor("from"); + + let spans = this.findSpansAt(cursor); + for(let span of spans) { + if(!span.isInline()) continue; + let loc = span.find(); + if(!loc) continue; + // If we're at the end of a bold span, we want to stop bolding. + if(samePosition(loc.to, cursor)) action = "remove"; + // If we're at the start of a bold span, we want to continue bolding. + if(samePosition(loc.from, cursor)) action = "add"; + // Otherwise we're somewhere in the middle, and want to insert some unbolded text. + else action = "split"; + } + this.formatting[source.type] = action; + } + this.finalizeLastHistoryEntry(); + }); + } + + formatLine(source:{type:string, level?:number, listData?: {type:"ordered"|"bullet", start?: number}}) { + this.finalizeLastHistoryEntry(); + let doc = this.cm.getDoc(); + this.cm.operation(() => { + let from = doc.getCursor("from"); + let to = doc.getCursor("to"); + + // No editor-controlled span may be created within a codeblock. + // @NOTE: This feels like a minor layor violation. + if(from.line !== to.line && this.findSpans(from, to, "code_block").length || this.findSpansAt(from, "code_block").length) return; + + let existing:Span[] = []; + let formatted = false; + for(let line = from.line, end = to.line; line <= end; line++) { + let cur = {line, ch: 0}; + + // Line formats are exclusive, so we clear intersecting line spans of other types. + let spans = this.findSpansAt(cur); + for(let span of spans) { + if(span.isLine() && span.source.type !== source.type) { + span.clear(); + } + } + + spans = this.findSpansAt(cur, source.type); + // If this line isn't already formatted to this type, format it. + if(!spans.length) { + this.formatSpan(cur, cur, source); + formatted = true; + // Otherwise store the span. We may need to clear them if we intend to unformat the selection. + } else { + existing.push.apply(existing, spans); + } + } + + // If no new lines were formatted, we mean to clear the existing format. + if(!formatted) { + for(let span of existing) { + span.clear(); + } + } + + this.finalizeLastHistoryEntry(); + this.refresh(); + }); + } + + formatBlock(source:{type:string}) { + this.finalizeLastHistoryEntry(); + let doc = this.cm.getDoc(); + this.cm.operation(() => { + let from = {line: doc.getCursor("from").line, ch: 0}; + let to = {line: doc.getCursor("to").line + 1, ch: 0}; + + if(doc.getLine(to.line) !== "") { + let cursor = doc.getCursor(); + doc.replaceRange("\n", to, to, "+normalize"); + doc.setCursor(cursor); + } + + // Determine if a block span in this range already exists. + let exists:Span|undefined; + let existing = this.findSpansAt(from, source.type); + for(let span of existing) { + let loc = span.find(); + if(!loc) continue; + exists = span; + break; + } + + // If the span already exists, we mean to clear it. + if(exists) { + exists.clear(); + + // We're creating a new span. + } else { + + // Block formats are exclusive, so we clear intersecting spans of other types. + let spans = this.findSpans(doc.posFromIndex(doc.indexFromPos(from) - 1), to); + for(let span of spans) { + if(span.isEditorControlled()) { + span.clear(); + } + } + + this.formatSpan(from, to, source); + } + }); + } + + trackDenormalized(span:Span) { + if(span.isDenormalized) { + let denormalized = span.isDenormalized(); + let existingIx = this.denormalizedSpans.indexOf(span); + if(denormalized && existingIx === -1) { + this.denormalizedSpans.push(span); + } else if(!denormalized && existingIx !== -1) { + this.denormalizedSpans.splice(existingIx, 1); + } + } + } + + //------------------------------------------------------- + // Undo History + //------------------------------------------------------- + undo = () => { + let history = this.history; + // We're out of undo steps. + if(history.position === 0) return; + this.finalizeLastHistoryEntry(); // @FIXME: wut do? + history.position--; + let changeSet = history.items[history.position] + this._historyDo(changeSet, true); + } + + redo = () => { + let history = this.history; + // We're out of redo steps. + if(history.position > history.items.length - 1) return; + let changeSet = history.items[history.position] + history.position++; + this._historyDo(changeSet); + } + + protected _historyDo(changeSet:HistoryItem, invert:boolean = false) { + this.history.transitioning = true; + let noRangeChanges = true; + this.cm.operation(() => { + let doc = this.cm.getDoc(); + for(let ix = 0, len = changeSet.changes.length; ix < len; ix++) { + let change = changeSet.changes[invert ? len - ix - 1 : ix]; + if(invert) change = change.invert(); + if(isRangeChange(change)) { + noRangeChanges = false; + let removedPos = doc.posFromIndex(doc.indexFromPos(change.from) + change.removedText.length); + doc.replaceRange(change.addedText, change.from, removedPos); + } else if(isSpanChange(change)) { + for(let removed of change.removed) { + removed.span.clear("+mdundo"); + } + for(let added of change.added) { + added.span.apply(added.from, added.to, "+mdundo"); + } + } + } + }); + + // Because updating the spans doesn't trigger a change, we can't rely on the changes handler to + // clear the transitioning state for us if we don't have any range changes. + if(noRangeChanges) { + this.history.transitioning = false; + } + } + + addToHistory(change:Change|SpanChange) { + let history = this.history; + // Bail if we're currently doing an undo or redo + if(history.transitioning) return; + + // Truncate the history tree to ancestors of the current state. + // @NOTE: In a fancier implementation we could maintain branching history instead. + if(history.items.length > history.position) { + history.items.length = history.position; + } + let changeSet:HistoryItem; + // If the last history step hasn't been finalized, we want to keep glomming onto it. + let last = history.items[history.items.length - 1]; + if(last && !last.finalized) changeSet = last; + else changeSet = {changes: []}; + + // @FIXME: Is this check still necessary with history.transitioning? + if(change.origin !== "+mdundo" && change.origin !== "+mdredo") { + changeSet.changes.push(change); + } + // Finally add the history step to the history stack (if it's not already in there). + if(changeSet !== last) { + history.position++; + history.items.push(changeSet); + } + } + + finalizeLastHistoryEntry() { + let history = this.history; + if(!history.items.length) return; + history.items[history.items.length - 1].finalized = true; + } + + //------------------------------------------------------- + // Handlers + //------------------------------------------------------- + + injectCodeMirror:RenderHandler = (node:EditorNode, elem) => { + if(!node.cm) { + node.cm = this.cm; + node.appendChild(this.cm.getWrapperElement()); + } + this.cm.refresh(); + this.ide.render(); + } + + onBeforeChange = (raw:CodeMirror.EditorChangeCancellable) => { + this.dirty = true; + let doc = this.cm.getDoc(); + let change = new ChangeCancellable(raw); + let {from, to} = change; + let spans:Span[]; + if(samePosition(from, to)) { + spans = this.findSpansAt(from); + } else { + let inclusiveFrom = doc.posFromIndex(doc.indexFromPos(from) - 1); + let inclusiveTo = doc.posFromIndex(doc.indexFromPos(to) + 1); + spans = this.findSpans(inclusiveFrom, inclusiveTo); + } + + // Grab all of the line spans intersecting this change too. + for(let line = from.line, end = to.line; line <= end; line++) { + let maybeLineSpans = this.findSpansAt({line, ch: 0}); + for(let maybeLineSpan of maybeLineSpans) { + if(maybeLineSpan.isLine() && spans.indexOf(maybeLineSpan) === -1) { + spans.push(maybeLineSpan); + } + } + } + + for(let span of spans) { + let loc = span.find(); + if(!loc) { + span.clear(); + return; + } + + if(span.onBeforeChange) { + span.onBeforeChange(change); + } + + // If we clear the span lazily, we can't capture it's position for undo/redo + if(span.isInline() && comparePositions(change.from, loc.from) <= 0 && comparePositions(change.to, loc.to) >= 0) { + span.clear(change.origin); + } + } + + if(!change.canceled) { + this.changing = true; + if(this.changingSpans) { + this.changingSpans.push.apply(this.changingSpans, spans); + } else { + this.changingSpans = spans; + } + } + } + + onChange = (raw:CodeMirror.EditorChangeLinkedList) => { + let doc = this.cm.getDoc(); + this.cm.operation(() => { + let lastLine = doc.lastLine(); + let pos = CodeMirror.Pos(lastLine + 1, 0); + if(doc.getLine(lastLine) !== "") { + let cursor = doc.getCursor(); + doc.replaceRange("\n", pos, pos, "+normalize"); + doc.setCursor(cursor); + } + }); + + let change = new ChangeLinkedList(raw); + let spans = this.changingSpans || []; + if(change.origin === "+mdredo" || change.origin === "+mdundo") { + for(let span of spans) { + if(span.refresh) span.refresh(); + } + return; + } + + // Collapse multiline changes into their own undo step + if(change.text.length > 1) this.finalizeLastHistoryEntry(); + + + let cur:ChangeLinkedList|undefined = change; + let affectedLines = {}; + while(cur) { + affectedLines[cur.from.line] = true; + affectedLines[cur.to.line] = true; + affectedLines[cur.final.line] = true; + + this.addToHistory(cur); + cur = cur.next(); + } + for(let l in affectedLines) { + let line = +l; + let text = doc.getLine(line); + if(!text) continue; + let pos = {line, ch: 0}; + if((text[0] === " " || text[text.length - 1] === " ") && !this.inCodeBlock(pos)) { + let handled = false; + for(let span of this.findSpansAt(pos)) { + if(span.isLine()) { + handled = true; + break; + } + } + if(!handled) { + let span = this.markSpan(pos, pos, {type: "whitespace"}); + this.denormalizedSpans.push(span); + } + } + } + + for(let span of spans) { + if(!span.onChange) continue; + + if(!span.find()) span.clear(); + else { + let cur:ChangeLinkedList|undefined = change; + while(cur) { + span.onChange(cur); + cur = cur.next(); + } + } + } + + for(let span of spans) { + this.trackDenormalized(span); + } + + if(change.origin !== "+normalize") { + for(let format in this.formatting) { + let action = this.formatting[format]; + if(action === "add") { + let span = this.markSpan(change.from, change.final, {type: format}); + this.trackDenormalized(span); + } + } + } + + // We need to refresh in on change because line measurement information will get cached by CM before we hit onChanges. + // If we see lots of slowness when typing, this is a probable culprit and we can get smarter about this. + if(change.isNewlineChange()) { + for(let span of this.changingSpans) { + if(span.refresh) span.refresh(); + } + } + } + + onChanges = (raws:CodeMirror.EditorChangeLinkedList[]) => { + if(this.changingSpans) { + for(let span of this.changingSpans) { + if(span.refresh) span.refresh(); + } + } + this.changingSpans = undefined; + this.changing = false; + this.history.transitioning = false; + this.formatting = {}; + this.queueUpdate(); + } + + onCursorActivity = () => { + let doc = this.cm.getDoc(); + let cursor = doc.getCursor(); + + if(!this.changing) { + this.finalizeLastHistoryEntry(); + } + // Remove any formatting that may have been applied + this.formatting = {}; + + // If any spans are currently denormalized, attempt to normalize them if they're not currently being edited. + if(this.denormalizedSpans.length) { + console.log("Denormalized:", this.denormalizedSpans.length); + for(let ix = 0; ix < this.denormalizedSpans.length;) { + let span = this.denormalizedSpans[ix]; + let loc = span.find(); + if(!loc) span.clear(); + + // If the span is Inline or Block and our cursor is before or after it, we're clear to normalize. + else if((span.isInline() || span.isBlock()) && + (comparePositions(cursor, loc.from) < 0 || comparePositions(cursor, loc.to) > 0)) { + span.normalize() + + // If the span is a Line and our cursor is on a different line, we're clear to normalize. + } else if(span.isLine() && cursor.line !== loc.from.line) { + span.normalize(); + + // Otherwise the span remains denormalized. + } else { + ix++; + continue; + } + + console.log("- normalized", span); + if(this.denormalizedSpans.length > 1) { + this.denormalizedSpans[ix] = this.denormalizedSpans.pop(); + } else { + this.denormalizedSpans.pop(); + } + } + + // If everybody is normalized now, we can queue an update to resync immediately. + if(!this.denormalizedSpans.length) { + this.queueUpdate(); + } + } + + this.updateFormatters(); + } + + onScroll = () => { + this.updateFormatters(); + } + + updateFormatters = debounce(() => { + let doc = this.cm.getDoc(); + let cursor = doc.getCursor(); + + // If we're outside of a codeblock, display our rich text controls. + let codeBlocks = this.findSpansAt(cursor, "code_block"); + + //If the cursor is at the beginning of a new line, display the new block button. + let old = this.showNewBlockBar; + this.showNewBlockBar = (!codeBlocks.length && + cursor.ch === 0 && + doc.getLine(cursor.line) === ""); + + if(this.showNewBlockBar !== old) { + this.newBlockBar.active = false; + this.queueUpdate(); + } if(this.showNewBlockBar) { + this.queueUpdate(); + } + + // Otherwise if there's a selection, show the format bar. + let inputState = this.ide.inputState; + let modifyingSelection = inputState.mouse["1"] || inputState.keyboard.shift; + codeBlocks = this.findSpans(doc.getCursor("from"), doc.getCursor("to"), "code_block"); + + old = this.showFormatBar; + this.showFormatBar = (!modifyingSelection && !codeBlocks.length && doc.somethingSelected()); + if(this.showFormatBar !== old || this.showFormatBar) this.queueUpdate(); + }, 30); + + // Elements + + // @NOTE: Does this belong in the IDE? + controls() { + let inspectorButton:Elem = {c: "inspector-button ion-wand", text: "", title: "Inspect", click: () => this.ide.toggleInspecting()}; + if(this.ide.inspectingClick) inspectorButton.c += " waiting"; + else if(this.ide.inspecting) inspectorButton.c += " inspecting"; + + return {c: "flex-row controls", children: [ + {c: "ion-refresh", title: "Reset (⌃⇧⏎ or ⇧⌘⏎ )", click: () => this.ide.eval(false)}, + {c: "ion-ios-play", title: "Run (⌃⏎ or ⌘⏎)", click: () => this.ide.eval(true)}, + inspectorButton + ]}; + } + + render() { + return {c: "editor-pane", postRender: this.injectCodeMirror, children: [ + this.controls(), + this.showNewBlockBar ? newBlockBar(this.newBlockBar) : undefined, + this.showFormatBar ? formatBar({editor: this}) : undefined + ]}; + } +} + +//--------------------------------------------------------- +// Comments +//--------------------------------------------------------- +/* - [x] Comments are pinned to a range in the current CM editor + * - [x] Hovering (always?) a comment will highlight its matching Position or Range + * - [x] Clicking a comment will scroll its location into view + * - [x] Comments are collapsed by the callback that moves them into position by doing so in order + * - [x] Last priority on width + * - [x] Soak up left over space + * - [x] Icons below min width + * - [x] Popout view on hover when iconified + * - [x] BUG: Mouse left of comments-panel in icon popout causes popout to close + * - [ ] BUG: Display popout above instead of below when below would push it off the screen + * - [ ] inline comments for narrow screens + * - [ ] Condensed, unattached console view + * - [ ] Count indicator (?) + * - [x] Scrollbar minimap + * - [ ] Filters (?) + * - [ ] Quick actions + * - [x] Map of ids to QA titles, description templates, and executors + * - [ ] Hovering a quick action will display a transient tooltip beneath the action bar describing the impact of clicking it + * - [ ] All QAs must be undo-able + * - [ ] AESTHETIC: selection goes under comments bar (instead of respecting line margins) + * - Comment types: + * - Errors + * - Warnings + * - View results + * - Live docs + * - User messages / responses + */ + +type CommentType = "error"|"warning"|"info"|"comment"|"result"; +interface Comment { + loc: Position|Range, + type: CommentType, + title?: string, + description?: string, + actions?: string[], + replies?: string[], + + marker?: CodeMirror.TextMarker, + annotation?: CodeMirror.AnnotateScrollbar.Annotation +} +interface CommentMap {[id:string]: Comment} +interface Action { + name: string, + description: (comment:Comment) => string, + run: (event:Event, {commentId:string}) => void +} + +class Comments { + comments:{[id:string]: DocumentCommentSpan} = {}; + ordered:string[]; + + active?:string; + rootNode?:HTMLElement; + _currentWidth?:number; + + constructor(public ide:IDE) { + this.update(); + } + + collapsed() { + return this._currentWidth <= 300; + } + + update() { + let touchedIds = {}; + for(let span of this.ide.editor.getAllSpans("document_comment") as DocumentCommentSpan[]) { + let commentId = span.id; + touchedIds[commentId] = true; + if(this.comments[commentId]) continue; + this.comments[commentId] = span; + } + + for(let commentId in this.comments) { + if(!touchedIds[commentId]) { + this.comments[commentId].clear(); + delete this.comments[commentId]; + } + } + + this.ordered = Object.keys(this.comments); + this.ordered.sort((a, b) => compareSpans(this.comments[a], this.comments[b])); + } + + highlight = (event:MouseEvent, {commentId}) => { + let comment = this.comments[commentId]; + this.active = commentId; + let loc = comment.find(); + if(!loc) return; + + // @TODO: Separate highlighted span + } + + unhighlight = (event:MouseEvent, {commentId}) => { + let comment = this.comments[commentId]; + this.active = undefined; + let loc = comment.find(); + if(!loc) return; + + // @TODO: Remove separate highlighted span. + } + + goTo = (event, {commentId}) => { + let comment = this.comments[commentId]; + let cm = this.ide.editor.cm; + let loc = comment.find(); + if(!loc) return; + cm.scrollIntoView(loc, 20); + } + + openComment = (event, {commentId}) => { + this.active = commentId; + this.ide.render(); + } + + closeComment = (event, {commentId}) => { + this.active = undefined; + this.ide.render(); + } + + inject = (node:HTMLElement, elem:Elem) => { + let {commentId} = elem; + let comment = this.comments[commentId]; + + if(comment.commentElem) { + comment.commentElem.appendChild(node); + } + } + + comment(commentId:string):Elem { + let comment = this.comments[commentId]; + if(!comment) return; + let actions:Elem[] = []; + + return { + c: `comment ${comment.kind}`, commentId, dirty: true, + postRender: this.inject, + mouseover: this.highlight, mouseleave: this.unhighlight, click: this.goTo, + children: [ + {c: "comment-inner", children: [ + comment.message ? {c: "message", text: comment.message} : undefined, + actions.length ? {c: "quick-actions", children: actions} : undefined, + ]} + ]}; + } + + render():Elem { // @FIXME: I'm here, just hidden by CodeMirror and CM scroll + let children:Elem[] = []; + for(let commentId of this.ordered) { + children.push(this.comment(commentId)); + } + return {c: "comments-pane", children}; + } +} + +//--------------------------------------------------------- +// Format Bar +//--------------------------------------------------------- + +/* - Anchors under selection + * - Suppressed by shift key (modifying selection) + * - Text: B / I / H / Code + * - Code: Something's wrong + */ + +interface EditorBarElem extends Elem { editor: Editor, active?: boolean } + +function formatBar({editor}:EditorBarElem):Elem { + let doc = editor.cm.getDoc(); + let cursor = doc.getCursor("to"); + let bottom = editor.cm.cursorCoords(cursor, undefined).bottom; + let left = editor.cm.cursorCoords(cursor, "local").left; + + return {id: "format-bar", c: "format-bar", top: bottom, left: left, children: [ + {text: "B", click: () => editor.format({type: "strong"}, true)}, + {text: "I", click: () => editor.format({type: "emph"}, true)}, + {text: "code", click: () => editor.format({type: "code"}, true)}, + {text: "H1", click: () => editor.format({type: "heading", level: 1}, true)}, + {text: "H2", click: () => editor.format({type: "heading", level: 2}, true)}, + {text: "H3", click: () => editor.format({type: "heading", level: 3}, true)}, + {text: "block", click: () => editor.format({type: "code_block"}, true)}, + ]}; +} + +//--------------------------------------------------------- +// New Block +//--------------------------------------------------------- + +/* - Button in left margin + * - Only appears on blank lines with editor focused + * - Text: Block / List / Quote / H(?) + */ + +function newBlockBar(elem:EditorBarElem):Elem { + let {editor, active} = elem; + let doc = editor.cm.getDoc(); + let cursor = doc.getCursor(); + let top = editor.cm.cursorCoords(cursor, undefined).top; + let left = 0; + + return {id: "new-block-bar", c: `new-block-bar ${active ? "active" : ""}`, top, left, children: [ + {c: "new-block-bar-toggle ion-plus", click: () => { + elem.active = !elem.active; + editor.cm.focus(); + editor.queueUpdate(); + }}, + {c: "flex-row controls", children: [ + {text: "block", click: () => editor.format({type: "code_block"}, true)}, + {text: "list", click: () => editor.format({type: "item"}, true)}, + {text: "H1", click: () => editor.format({type: "heading", level: 1}, true)}, + {text: "H2", click: () => editor.format({type: "heading", level: 2}, true)}, + {text: "H3", click: () => editor.format({type: "heading", level: 3}, true)} + ]} + ]}; +} + +//--------------------------------------------------------- +// Modals +//--------------------------------------------------------- + +/* - Transient + * - Anchors to bottom of screen + * - Scrolls targeted element back into view, if any + * - Modals: + * - Something's wrong + */ + +function modalWrapper():Elem { + return {}; +} + + +//--------------------------------------------------------- +// Root +//--------------------------------------------------------- + +export class IDE { + protected _fileCache:{[fileId:string]: string} = {}; + + /** The id of the active document. */ + documentId?:string; + /** Whether the active document has been loaded. */ + loaded = false; + /** Whether the IDE is currently loading a new document. */ + loading = false; + /** The current editor generation. Used for imposing a relative ordering on parses. */ + generation = 0; + /** Whether the currently open document is a modified version of an example. */ + modified = false; + /** Whether or not files are stored and operated on purely locally */ + local = false; + + /** Whether the inspector is currently active. */ + inspecting = false; + + /** Whether the next click should be an inspector click automatically (as opposed to requiring Cmd or Ctrl modifiers. */ + inspectingClick = false; + + renderer:Renderer = new Renderer(); + + notices:{message: string, type: string, time: number}[] = []; + + languageService:LanguageService = new LanguageService(); + navigator:Navigator = new Navigator(this); + editor:Editor = new Editor(this); + comments:Comments = new Comments(this); + + constructor( ) { + document.body.appendChild(this.renderer.content); + this.renderer.content.classList.add("ide-root"); + + this.enableInspector(); + this.monitorInputState(); + } + + elem() { + return {c: `editor-root`, children: [ + this.navigator.render(), + {c: "main-pane", children: [ + this.noticesElem(), + this.editor.render(), + ]}, + this.comments.render() + ]}; + } + + noticesElem() { + let items = []; + for(let notice of this.notices) { + let time = new Date(notice.time); + let formattedMinutes = time.getMinutes() >= 10 ? time.getMinutes() : `0${time.getMinutes()}`; + let formattedSeconds = time.getSeconds() >= 10 ? time.getMinutes() : `0${time.getSeconds()}`; + items.push({c: `notice ${notice.type} flex-row`, children: [ + {c: "time", text: `${time.getHours()}:${formattedMinutes}:${formattedSeconds}`}, + {c: "message", text: notice.message} + ]}); + } + + if(items.length) { + return {c: "notices", children: items}; + } + } + + render() { + // Update child states as necessary + this.renderer.render([this.elem()]); + } + + queueUpdate = debounce((shouldEval = false) => { + if(this.editor.dirty) { + this.generation++; + if(this.onChange) this.onChange(this); + this.editor.dirty = false; + + client.sendEvent([{tag: ["inspector", "clear"]}]); + this.saveDocument(); + + if(shouldEval) { + if(this.documentId === "quickstart.eve") { + this.eval(false); + } else { + this.eval(true); + } + } + } + this.render(); + }, 1, true); + + loadFile(docId:string, content?:string) { + if(!docId) return false; + // if we're not in local mode, file content is going to come from + // some other source and we should just load it directly + if(!this.local && content !== undefined) { + this.documentId = docId; + this.editor.reset(); + this.notices = []; + this.loading = true; + this.onLoadFile(this, docId, content); + return true; + } else if(this.loading || this.documentId === docId) { + return false; + } + + // Otherwise we load the file from either localstorage or from the supplied + // examples object + let saves = JSON.parse(localStorage.getItem("eve-saves") || "{}"); + let code = saves[docId]; + if(code) { + this.modified = true; + } else { + code = this._fileCache[docId]; + this.modified = false; + } + if(code === undefined) { + console.error(`Unable to load uncached file: '${docId}'`); + return false; + } + this.loaded = false; + this.documentId = docId; + this.editor.reset(); + this.notices = []; + this.loading = true; + this.onLoadFile(this, docId, code); + + return true; + } + + loadWorkspace(directory:string, files:{[filename:string]: string}) { + // @FIXME: un-hardcode root to enable multiple WS's. + this._fileCache = files; + this.navigator.loadWorkspace("root", directory, files); + } + + loadDocument(generation:number, text:string, packed:any[], attributes:{[id:string]: any|undefined}) { + if(generation < this.generation && generation !== undefined) return; + if(this.loaded) { + this.editor.updateDocument(packed, attributes); + } else { + this.editor.loadDocument(this.documentId, text, packed, attributes); + this.loaded = true; + this.loading = false; + } + + if(this.documentId) { + let name = this.documentId; // @FIXME + this.navigator.loadDocument(this.documentId, name); + this.navigator.currentId = this.documentId; + this.comments.update(); + } else { + // Empty file + } + + this.render(); + } + + saveDocument() { + if(!this.documentId || !this.loaded) return; + + let md = this.editor.toMarkdown(); + + // if we're not local, we notify the outside world that we're trying + // to save + if(!this.local) { + return this.onSaveDocument(this, this.documentId, md); + } + + // othewise, save it to local storage + let saves = JSON.parse(localStorage.getItem("eve-saves") || "{}"); + if(md !== this._fileCache[this.documentId]) { + saves[this.documentId] = md; + this.modified = true; + } else { + this.modified = false; + } + localStorage.setItem("eve-saves", JSON.stringify(saves)); + } + + revertDocument() { + if(!this.documentId || !this.loaded) return; + let docId = this.documentId; + let saves = JSON.parse(localStorage.getItem("eve-saves") || "{}"); + delete saves[docId]; + localStorage.setItem("eve-saves", JSON.stringify(saves)); + this.documentId = undefined; + this.loadFile(docId); + } + + createDocument(folder:string) { + let newId:string|undefined; + let ix = 0; + while(!newId) { + newId = `/${folder}/untitled${ix ? "-" + ix : ""}.eve`; + if(this._fileCache[newId]) newId = undefined; + } + let emptyTemplate = `# Untitled`; + this._fileCache[newId] = emptyTemplate; + // @FIXME: Need a way to side-load a single node that isn't hardwired to a span. + // Split the current updateNode up. + // @FIXME: This won't work with multiple workspaces obviously. + this.loadWorkspace("examples", this._fileCache); + if(this.onSaveDocument) this.onSaveDocument(this, newId, emptyTemplate); + this.loadFile(newId); + } + + injectSpans(packed:any[], attributes:{[id:string]: any|undefined}) { + this.editor.injectSpans(packed, attributes); + this.comments.update(); + this.render(); + } + + injectNotice(type:string, message:string) { + let time = Date.now(); + this.notices.push({type, message, time}); + this.render(); + this.editor.cm.refresh(); + } + + eval(persist?: boolean) { + if(this.notices.length) { + this.notices = []; + this.render(); + this.editor.cm.refresh(); + } + if(this.onEval) this.onEval(this, persist); + } + + tokenInfo() { + let doc = this.editor.cm.getDoc(); + let cursor = doc.getCursor(); + let spans = this.editor.findSpansAt(cursor).filter((span) => span instanceof Spans.ParserSpan); + if(spans.length && this.onTokenInfo) { + this.onTokenInfo(this, spans[0].source.id); + } + } + + monitorInputState() { + window.addEventListener("mousedown", this.updateMouseInputState); + window.addEventListener("mouseup", this.updateMouseInputState); + window.addEventListener("keydown", this.updateKeyboardInputState); + window.addEventListener("keyup", this.updateKeyboardInputState); + } + + inputState = { + mouse: {1: false}, + keyboard: {shift: false} + } + updateMouseInputState = (event:MouseEvent) => { + let mouse = this.inputState.mouse; + let neue = !!(event.buttons & 1); + if(!neue && mouse["1"]) this.editor.updateFormatters(); + mouse["1"] = neue; + } + updateKeyboardInputState = (event:KeyboardEvent) => { + let keyboard = this.inputState.keyboard; + let neue = event.shiftKey; + if(!neue && keyboard.shift) this.editor.updateFormatters(); + keyboard.shift = neue; + } + + //------------------------------------------------------- + // Actions + //------------------------------------------------------- + + activeActions:{[recordId:string]: any} = {}; + + actions = { + insert: { + "mark-between": (action) => { + let source = {type: action.type[0]}; + for(let attribute in action) { + if(action[attribute] === undefined) continue; + source[attribute] = action[attribute].length === 1 ? action[attribute][0] : action[attribute]; + } + + if(action.span) { + action.spans = this.editor.markBetween(action.span, source, action.bounds); + } + + if(action.range) { + let doc = this.editor.cm.getDoc(); + action.spans = action.spans || []; + let ranges:Range[] = []; + for(let rangeId of action.range) { + let rangeRecord = indexes.records.index[rangeId]; + if(!rangeRecord || !rangeRecord.start || !rangeRecord.stop) continue; + + ranges.push({from: doc.posFromIndex(rangeRecord.start[0]), to: doc.posFromIndex(rangeRecord.stop[0])}); + } + action.spans.push.apply(action.spans, this.editor.markBetween(ranges, source, action.bounds)); + } + }, + + "mark-span": (action) => { + action.spans = []; + + let ranges:Range[] = []; + if(action.span) { + for(let spanId of action.span) { + let span = this.editor.getSpanBySourceId(spanId); + let range = span && span.find(); + if(span.isBlock() && action.type[0] === "document_widget") { // @FIXME: This is a horrible hack to deal with blocks ending on the next line. + range = {from: range.from, to: {line: range.to.line - 1, ch: 0}}; + } + if(range) ranges.push(range); + } + } + + let source = {type: action.type[0]}; + for(let attribute in action) { + if(action[attribute] === undefined) continue; + source[attribute] = action[attribute].length === 1 ? action[attribute][0] : action[attribute]; + } + + for(let range of ranges) { + action.spans.push(this.editor.markSpan(range.from, range.to, source)); + } + }, + + "mark-range": (action) => { + let source = {type: action.type[0]}; + for(let attribute in action) { + let value = action[attribute]; + if(value === undefined) continue; + source[attribute] = value.length === 1 ? value[0] : value; + } + + let doc = this.editor.cm.getDoc(); + let start = doc.posFromIndex(action.start[0]); + let stop = doc.posFromIndex(action.stop[0]); + action.span = this.editor.markSpan(start, stop, source); + }, + + "jump-to": (action) => { + let from:Position; + + if(action.position) { + let doc = this.editor.cm.getDoc(); + let min = Infinity; + for(let index of action.position) { + if(index < min) min = index; + } + from = doc.posFromIndex(min) + } + + if(action.span) { + for(let spanId of action.span) { + let span = this.editor.getSpanBySourceId(spanId); + if(!span) continue; + let loc = span.find(); + if(!loc) continue; + if(!from || comparePositions(loc.from, from) < 0) from = loc.from; + } + } + + if(from) { + this.editor.scrollToPosition(from); + } + }, + + "find-section": (action, actionId) => { + let doc = this.editor.cm.getDoc(); + let records = []; + if(action.position) { + for(let index of action.position) { + let pos = doc.posFromIndex(index); + let heading = this.editor.findHeadingAt(pos); + if(heading) { + let range = heading.getSectionRange(); + records.push({tag: ["section", "editor"], position: index, heading: heading.source.id, start: doc.indexFromPos(range.from), stop: doc.indexFromPos(range.to)}); + } else { + records.push({tag: ["section", "editor"], position: index, start: 0, stop: doc.getValue().length}); + } + } + } + if(action.span) { + for(let spanId of action.span as string[]) { + let span = this.editor.getSpanBySourceId(spanId); + if(!span) continue; + let loc = span.find(); + if(!loc) continue; + + let pos = loc.from; + let heading = this.editor.findHeadingAt(pos); + if(heading) { + let range = heading.getSectionRange(); + records.push({tag: ["section", "editor"], span: spanId, heading: heading.source.id, start: doc.indexFromPos(range.from), stop: doc.indexFromPos(range.to)}); + } else { + records.push({tag: ["section", "editor"], span: spanId, start: 0, stop: doc.getValue().length}); + } + } + } + + if(records.length) { + for(let record of records) { + record.action = actionId; + } + client.sendEvent(records); + } + }, + + "elide-between-sections": (action, actionId) => { + let doc = this.editor.cm.getDoc(); + + let visibleHeadings:HeadingSpan[] = []; + if(action.position) { + for(let index of action.position) { + let pos = doc.posFromIndex(index); + let heading = this.editor.findHeadingAt(pos); + if(heading) visibleHeadings.push(heading); + } + } + if(action.span) { + for(let spanId of action.span as string[]) { + let span = this.editor.getSpanBySourceId(spanId); + if(!span) continue; + let loc = span.find(); + if(!loc) continue; + + let pos = loc.from; + let heading = this.editor.findHeadingAt(pos); + if(heading) visibleHeadings.push(heading); + } + } + + let headings = this.editor.getAllSpans("heading") as HeadingSpan[]; + for(let heading of headings) { + if(visibleHeadings.indexOf(heading) === -1) { + heading.hide(); + } else { + heading.unhide(); + } + } + this.navigator.updateElision(); + }, + + "find-source": (action, actionId) => { + let record = action.record && action.record[0]; + let attribute = action.attribute && action.attribute[0]; + let span = action.span && action.span[0]; + this.languageService.findSource({record, attribute, span}, this.languageService.unpackSource((records) => { + for(let record of records) { + record.tag.push("editor"); + record["action"] = actionId; + } + client.sendEvent(records); + })); + }, + + "find-related": (action, actionId) => { + this.languageService.findRelated({span: action.span, variable: action.variable}, this.languageService.unpackRelated((records) => { + for(let record of records) { + record.tag.push("editor"); + record["action"] = actionId; + } + client.sendEvent(records); + })); + }, + + "find-value": (action, actionId) => { + let given; + if(action.given) { + given = {}; + for(let avId of action.given) { + let av = indexes.records.index[avId]; + given[av.attribute] = av.value; + } + } + + this.languageService.findValue({variable: action.variable, given}, this.languageService.unpackValue((records) => { + let doc = this.editor.cm.getDoc(); + for(let record of records) { + record.tag.push("editor"); + record["action"] = actionId; + } + client.sendEvent(records); + })); + }, + + "find-cardinality": (action, actionId) => { + this.languageService.findCardinality({variable: action.variable}, this.languageService.unpackCardinality((records) => { + for(let record of records) { + record.tag.push("editor"); + record["action"] = actionId; + } + client.sendEvent(records); + })); + }, + + "find-affector": (action, actionId) => { + this.languageService.findAffector( + { + record: action.record && action.record[0], + attribute: action.attribute && action.attribute[0], + span: action.span && action.span[0] + }, + this.languageService.unpackAffector((records) => { + for(let record of records) { + record.tag.push("editor"); + record["action"] = actionId; + } + client.sendEvent(records); + })); + }, + + "find-failure": (action, actionId) => { + this.languageService.findFailure({block: action.block}, this.languageService.unpackFailure((records) => { + for(let record of records) { + record.tag.push("editor"); + record["action"] = actionId; + } + client.sendEvent(records); + })); + }, + + "find-root-drawers": (action, actionId) => { + this.languageService.findRootDrawer(null, this.languageService.unpackRootDrawer((records) => { + for(let record of records) { + record.tag.push("editor"); + record["action"] = actionId; + } + client.sendEvent(records); + })); + }, + + "find-performance": (action, actionId) => { + this.languageService.findPerformance(null, this.languageService.unpackPerformance((records) => { + for(let record of records) { + record.tag.push("editor"); + record["action"] = actionId; + } + client.sendEvent(records); + })); + }, + + "inspector": (action, actionId) => { + this.inspecting = true; + let inspectorElem:HTMLElement = activeElements[actionId] as any; + if(!inspectorElem) return; + if(action["in-editor"]) this.editor.cm.getWrapperElement().appendChild(inspectorElem); + + if(action.x && action.y) { + inspectorElem.style.position = "absolute"; + inspectorElem.style.left = action.x[0]; + inspectorElem.style.top = action.y[0]; + } + this.queueUpdate(); + } + }, + + remove: { + "mark-between": (action) => { + if(!action.spans) return; + for(let span of action.spans) { + span.clear(); + } + }, + + "mark-span": (action) => { + if(!action.spans) return; + for(let span of action.spans) { + span.clear(); + } + }, + + "mark-range": (action) => { + if(!action.span) return; + action.span.clear(); + }, + + "elide-between-sections": (action, actionId) => { + for(let span of this.editor.getAllSpans("elision")) { + span.clear(); + } + }, + + "inspector": (action, actionId) => { + this.inspecting = false; + this.queueUpdate(); + } + }, + }; + + updateActions(inserts: string[], removes: string[], records) { + this.editor.cm.operation(() => { + for(let recordId of removes) { + let action = this.activeActions[recordId]; + if(!action) return; + let run = this.actions.remove[action.tag]; + //console.log("STOP", action.tag, recordId, action, !!run); + if(run) run(action); + delete this.activeActions[recordId]; + } + + for(let recordId of inserts) { + let record = records[recordId]; + let bounds:Range|undefined; + if(record.within) { + let span = this.editor.getSpanBySourceId(record.within[0]); + if(span) bounds = span.find(); + } + + let action:any = {bounds}; + for(let tag of record.tag) { + if(tag in this.actions.insert || tag in this.actions.remove) { + action.tag = tag; + break; + } + } + if(!action.tag) continue; + + for(let attr in record) { + if(!action[attr]) action[attr] = record[attr]; + } + this.activeActions[recordId] = action; + + let run = this.actions.insert[action.tag]; + //console.log("START", action.tag, recordId, action, !!run); + if(!run) console.warn(`Unable to run unknown action type '${action.tag}'`, recordId, record); + else run(action, recordId); + } + }); + } + + //------------------------------------------------------- + // Views + //------------------------------------------------------- + activeViews:any = {}; + + updateViews(inserts: string[], removes: string[], records) { + for(let recordId of removes) { + let view = this.activeViews[recordId]; + if(!view) continue; + // Detach view + if(view.widget) view.widget.clear(); + view.widget = undefined; + } + + for(let recordId of inserts) { + // if the view already has a parent, leave it be. + if(indexes.byChild.index[recordId]) continue; + + // If the view is already active, he doesn't need inserted again. + if(this.activeViews[recordId] && this.activeViews[recordId].widget) continue; + + // Otherwise, we'll grab it and attach it to its creator in the editor. + let record = records[recordId]; + let view = this.activeViews[recordId] = this.activeViews[recordId] || {record: recordId, container: document.createElement("div")}; + view.container.className = "view-container"; + + //this.attachView(recordId, record.node) + // Find the source node for this view. + if(record.span) { + this.attachView(recordId, record.span[0]); + } else if(record.node) { + client.send({type: "findNode", recordId, node: record.node[0]}); + } else { + console.warn("Unable to parent view that doesn't provide its origin node or span id", record); + } + + } + } + + attachView(recordId:string, spanId:string) { + let view = this.activeViews[recordId]; + + // @NOTE: This isn't particularly kosher. + let node = activeElements[recordId]; + if(!node) return; + if(node !== view.container.firstChild) { + view.container.appendChild(node); + } + + let sourceSpan:Span|undefined = view.span; + if(spanId !== undefined) { + sourceSpan = this.editor.getSpanBySourceId(spanId); + } + + if(!sourceSpan) return; + view.span = sourceSpan; + + let loc = sourceSpan.find(); + if(!loc) return; + let line = loc.to.line; + if(sourceSpan.isBlock()) line -= 1; + + if(view.widget && line === view.line) return; + + if(view.widget) { + view.widget.clear(); + } + + view.line = line; + view.widget = this.editor.cm.addLineWidget(line, view.container); + } + + //------------------------------------------------------- + // Inspector + //------------------------------------------------------- + + findPaneAt(x: number, y: number):"editor"|"application"|undefined { + let editorContainer = this.editor.cm.getWrapperElement(); + let editor = editorContainer && editorContainer.getBoundingClientRect(); + let appContainer = document.querySelector(".application-container") + let app = appContainer && appContainer.getBoundingClientRect(); // @FIXME: Not particularly durable + if(editor && x >= editor.left && x <= editor.right && + y >= editor.top && y <= editor.bottom) { + return "editor"; + } else if(app && x >= app.left && x <= app.right && + y >= app.top && y <= app.bottom) { + return "application"; + } + } + + enableInspector() { + //window.addEventListener("mouseover", this.updateInspector); + window.addEventListener("click", this.updateInspector, true); + } + + disableInspector() { + //window.removeEventListener("mouseover", this.updateInspector); + window.removeEventListener("click", this.updateInspector, true); + } + + toggleInspecting() { + if(this.inspecting) { + client.sendEvent([{tag: ["inspector", "clear"]}]); + } else { + this.inspectingClick = true; + } + this.queueUpdate(); + } + + updateInspector = (event:MouseEvent) => { + let pane = this.findPaneAt(event.pageX, event.pageY); + if(!(event.ctrlKey || event.metaKey || this.inspectingClick)) return; + this.inspectingClick = false; + let events = []; + if(pane === "editor") { + let pos = this.editor.cm.coordsChar({left: event.pageX, top: event.pageY}); + let spans = this.editor.findSpansAt(pos).sort(compareSpans); + + let editorContainer = this.editor.cm.getWrapperElement(); + let bounds = editorContainer.getBoundingClientRect(); + let x = event.clientX - bounds.left; + let y = event.clientY - bounds.top; + + while(spans.length) { + let span = spans.shift(); + if(!span.isEditorControlled() || span.type === "code_block") { + events.push({tag: ["inspector", "inspect", spans.length === 0 ? "direct-target" : undefined], target: span.source.id, type: span.source.type, x, y}); + } + } + + } else if(pane === "application") { + let appContainer = document.querySelector(".application-root > .application-container > .program") as HTMLElement; + let x = event.clientX - appContainer.offsetLeft; + let y = event.clientY - appContainer.offsetTop; + let current:any = event.target; + while(current && current.entity) { + events.push({tag: ["inspector", "inspect", current === event.target ? "direct-target" : undefined], target: current.entity, type: "element", x, y}); + current = current.parentNode; + } + + // If we didn't click on an element, inspect the root. + if(events.length === 0) { + events.push({tag: ["inspector", "inspect", "direct-target"], type: "root", x, y}); + } + } + + this.queueUpdate(); + if(events.length) { + client.sendEvent(events); + event.preventDefault(); + event.stopPropagation(); + } + }; + + onChange?:(self:IDE) => void + onEval?:(self:IDE, persist?: boolean) => void + onLoadFile?:(self:IDE, documentId:string, code:string) => void + onTokenInfo?:(self:IDE, tokenId:string) => void + onSaveDocument?:(self:IDE, documentId:string, code:string) => void +} + +type FindSourceArgs = {record?: string, attribute?: string, span?:string|string[], source?: {block?: string[], span?: string[]}[]}; +type SourceRecord = {tag: string[], record?: string, attribute?: string, span: string[], block: string[]}; +type FindRelatedArgs = {span?: string[], variable?: string[]}; +type RelatedRecord = {tag: string[], span: string, variable: string[]}; +type FindValueArgs = {variable: string[], given: {[attribute: string]: any}, rows?: any[][], totalRows?: number, variableMappings?: {[span: string]: number}, variableNames?: {[span: string]: string}}; +type ValueRecord = {tag: string[], variable: string, value: any, row: number, name: string, register: number} +type FindCardinalityArgs = {variable: string[], cardinality?: {[variable: string]: number}}; +type CardinalityRecord = {tag: string[], variable: string, cardinality: number}; +type FindAffectorArgs = {record?: string, attribute?: string, span?: string, affector?: {block?: string[], action: string[]}[]}; +type AffectorRecord = {tag: string[], record?: string, attribute?: string, span?: string, block: string[], action: string[]}; +type FindFailureArgs = {block: string[], span?: {block: string, start: number, stop: number}[]}; +type FailureRecord = {tag: string[], block: string, start: number, stop: number}; +type FindRootDrawerArgs = {drawers?: {id: string, start: number, stop: number}[]}; +type RootDrawerRecord = {tag: string[], span: string, start: number, stop: number}; +type FindPerformanceArgs = {blocks?: {[blockId:string]: {avg: number, calls: number, color: string, max: number, min: number, percentFixpoint: number, time: number}}, fixpoint: {avg: number, count: number, time: number}}; +type PerformanceRecord = {tag: string[], block: string, average: number, calls: number, color: string, max: number, min: number, percent: number, total: number}; + +class LanguageService { + protected static _requestId = 0; + + protected _listeners:{[requestId:number]: (args:any) => void} = {}; + + findSource(args:FindSourceArgs, callback:(args:FindSourceArgs) => void) { + this.send("findSource", args, callback); + } + + unpackSource(callback:(args:SourceRecord[]) => void) { + return (message:FindSourceArgs) => { + let records:SourceRecord[] = []; + for(let source of message.source) { + let span:any = message.span || source.span; + records.push({tag: ["source"], record: message.record, attribute: message.attribute, span, block: source.block}); + } + callback(records); + }; + } + + findRelated(args:FindRelatedArgs, callback:(args:FindRelatedArgs) => void) { + this.send("findRelated", args, callback); + } + + unpackRelated(callback:(args:RelatedRecord[]) => void) { + return (message:FindRelatedArgs) => { + let records:RelatedRecord[] = []; + // This isn't really correct, but we're rolling with it for now. + for(let span of message.span) { + records.push({tag: ["related"], span, variable: message.variable}); + } + callback(records); + }; + } + + findValue(args:FindValueArgs, callback:(args:FindValueArgs) => void) { + this.send("findValue", args, callback); + } + + unpackValue(callback:(args:ValueRecord[]) => void) { + return (message:FindValueArgs) => { + if(message.totalRows > message.rows.length) { + // @TODO: Turn this into a fact. + console.warn(`Too many possible values, showing {{message.rows.length}} of {{message.totalRows}}`); + } + let mappings = message.variableMappings; + let names = message.variableNames; + let records:ValueRecord[] = []; + for(let rowIx = 0, rowCount = message.rows.length; rowIx < rowCount; rowIx++) { + let row = message.rows[rowIx]; + for(let variable in mappings) { + let register = mappings[variable]; + records.push({tag: ["value"], row: rowIx + 1, variable, value: row[register], register, name: names[variable]}); + } + } + callback(records); + }; + } + + findCardinality(args:FindCardinalityArgs, callback:(args:FindCardinalityArgs) => void) { + this.send("findCardinality", args, callback); + } + + unpackCardinality(callback:(args:CardinalityRecord[]) => void) { + return (message:FindCardinalityArgs) => { + let records:CardinalityRecord[] = []; + for(let variable in message.cardinality) { + records.push({tag: ["cardinality"], variable, cardinality: message.cardinality[variable]}); + } + callback(records); + }; + } + + findAffector(args:FindAffectorArgs, callback:(args:FindAffectorArgs) => void) { + this.send("findAffector", args, callback); + } + + unpackAffector(callback:(args:AffectorRecord[]) => void) { + return (message:FindAffectorArgs) => { + let records:AffectorRecord[] = []; + for(let affector of message.affector) { + records.push({tag: ["affector"], record: message.record, attribute: message.attribute, span: message.span, block: affector.block, action: affector.action}); + } + callback(records); + }; + } + + findFailure(args:FindFailureArgs, callback:(args:FindFailureArgs) => void) { + this.send("findFailure", args, callback); + } + + unpackFailure(callback:(args:FailureRecord[]) => void) { + return (message:FindFailureArgs) => { + let records:FailureRecord[] = []; + for(let failure of message.span) { + records.push({tag: ["failure"], block: failure.block, start: failure.start, stop: failure.stop}); + } + callback(records); + }; + } + + findRootDrawer(args:any, callback:(args:FindRootDrawerArgs) => void) { + this.send("findRootDrawers", args || {}, callback); + } + + unpackRootDrawer(callback:(args:RootDrawerRecord[]) => void) { + return (message:FindRootDrawerArgs) => { + let records:RootDrawerRecord[] = []; + for(let drawer of message.drawers) { + records.push({tag: ["root-drawer"], span: drawer.id, start: drawer.start, stop: drawer.stop}); + } + callback(records); + }; + } + + findPerformance(args:any, callback:(args:FindPerformanceArgs) => void) { + this.send("findPerformance", args || {}, callback); + } + + unpackPerformance(callback:(args:PerformanceRecord[]) => void) { + return (message:FindPerformanceArgs) => { + let records:PerformanceRecord[] = []; + for(let blockId in message.blocks) { + let block = message.blocks[blockId]; + records.push({tag: ["performance"], block: blockId, average: block.avg, calls: block.calls, color: block.color, max: block.max, min: block.min, percent: block.percentFixpoint, total: block.time}); + } + callback(records); + }; + } + + send(type:string, args:any, callback:any) { + let id = LanguageService._requestId++; + args.requestId = id; + this._listeners[id] = callback; + args.type = type; + //console.log("SENT", args); + client.send(args); + } + + handleMessage = (message) => { + let type = message.type; + if(type === "findSource" || type === "findRelated" || type === "findValue" || type === "findCardinality" || type === "findAffector" || type === "findFailure" || type === "findRootDrawers" || type === "findPerformance") { + let id = message.requestId; + let listener = this._listeners[id]; + if(listener) { + listener(message); + return true; + } + } + return false; + } +} diff --git a/src/ide/spans.ts b/src/ide/spans.ts new file mode 100644 index 000000000..d5a747f15 --- /dev/null +++ b/src/ide/spans.ts @@ -0,0 +1,1129 @@ +import * as CodeMirror from "codemirror"; +import {Editor, Change, ChangeCancellable} from "../ide"; +import {Range, Position, isRange, comparePositions, samePosition, whollyEnclosed, debounce} from "../util"; + +type FormatAction = "add"|"remove"|"split" + +function formattingChange(span:Span, change:Change, action?:FormatAction) { + let editor = span.editor; + let loc = span.find(); + if(!loc) return; + // Cut the changed range out of a span + if(action == "split") { + let final = change.final; + editor.markSpan(loc.from, change.from, span.source); + // If the change is within the right edge of the span, recreate the remaining segment + if(comparePositions(final, loc.to) === -1) { + editor.markSpan(final, loc.to, span.source); + } + span.clear(); + + } else if(!action) { + // If we're at the end of the span, expand it to include the change + if(samePosition(loc.to, change.from)) { + span.clear(); + editor.markSpan(loc.from, change.final, span.source); + } + } +} + +interface LineStyle { lineBackgroundClass?: string, lineTextClass?: string } + +function updateLineClasses(start:number, end:number, editor:Editor, {lineBackgroundClass, lineTextClass}:LineStyle) { + let cm = editor.cm; + if(start === end) { + let line = start + let info = cm.lineInfo(line); + if(lineBackgroundClass && (!info || !info.bgClass || info.bgClass.indexOf(lineBackgroundClass) === -1)) { + cm.addLineClass(line, "background", lineBackgroundClass); + } + if(lineTextClass && (!info || !info.textClass || info.textClass.indexOf(lineTextClass) === -1)) { + cm.addLineClass(line, "text", lineTextClass); + } + } + for(let line = start; line < end; line++) { + let info = cm.lineInfo(line); + if(lineBackgroundClass && (!info || !info.bgClass || info.bgClass.indexOf(lineBackgroundClass) === -1)) { + cm.addLineClass(line, "background", lineBackgroundClass); + } + if(lineTextClass && (!info || !info.textClass || info.textClass.indexOf(lineTextClass) === -1)) { + cm.addLineClass(line, "text", lineTextClass); + } + } +} + +function clearLineClasses(start:number, end:number, editor:Editor, {lineBackgroundClass, lineTextClass}:LineStyle) { + let cm = editor.cm; + if(start === end) { + let line = start; + if(lineBackgroundClass) cm.removeLineClass(line, "background", lineBackgroundClass); + if(lineTextClass) cm.removeLineClass(line, "text", lineTextClass); + } + for(let line = start; line < end; line++) { + if(lineBackgroundClass) cm.removeLineClass(line, "background", lineBackgroundClass); + if(lineTextClass) cm.removeLineClass(line, "text", lineTextClass); + } +} +//--------------------------------------------------------- +// Generic Spans +//--------------------------------------------------------- + +/** A SpanSource is the underlying representation of the span shared by the parser service and editor. */ +interface SpanSource { + /** One of the managed editor types (e.g. "strong") or an arbitrary other type managed by the parser service. */ + type: string, + /** The source id is the mapped token id used by the parser. */ + id: string +} + +/** A SpanMarker is a monkey-patched TextMarker that references its parent. */ +export interface SpanMarker extends CodeMirror.TextMarker { + span?: Span +} + +export function isSpanMarker(x:CodeMirror.TextMarker): x is SpanMarker { + return x && x["span"]; +} + +export function isEditorControlled(type:string) { + return spanTypes[type] && spanTypes[type]["_editorControlled"] || false; +} + +export function compareSpans(a, b) { + let aLoc = a.find(); + let bLoc = b.find(); + if(!aLoc && !bLoc) return 0; + if(!aLoc) return -1; + if(!bLoc) return 1; + if(aLoc.from.line === bLoc.from.line) { + if(aLoc.from.ch === bLoc.from.ch) return 0; + return aLoc.from.ch < bLoc.from.ch ? -1 : 1; + } + return aLoc.from.line < bLoc.from.line ? -1 : 1; +} + +export class Span { + protected static _nextId = 0; + + protected static _editorControlled = true; + protected _editorControlled = true; + protected static _spanStyle:"inline"|"line"|"block"; + protected _spanStyle:"inline"|"line"|"block"; + + /** Whether the span is currently elided. */ + protected hidden = false; + + id: string; + editor: Editor; + marker?: SpanMarker; + + type: string; + + protected _attributes:CodeMirror.TextMarkerOptions&{widget?: HTMLElement} = {}; + + constructor(editor:Editor, from:Position, to:Position, public source:SpanSource, origin = "+input") { + this.editor = editor; + if(!source.type) throw new Error("Unable to initialize Span without a type."); + this.type = source.type; + this.id = `${this.type}_${Span._nextId++}`; + this.apply(from, to, origin); + } + + apply(from:Position, to:Position, origin = "+input") { + if(this.marker) { + let loc = this.find(); + if(!loc || !samePosition(from, loc.from) || !samePosition(to, loc.to)) { + this.marker.clear(); + this.marker = this.marker.span = undefined; + } else { + // Nothing has changed. + return; + } + } + this._attributes.className = this._attributes.className || this.type; + let doc = this.editor.cm.getDoc(); + if(samePosition(from, to)) { + this.marker = doc.setBookmark(from, this._attributes); + } else { + this.marker = doc.markText(from, to, this._attributes); + } + this.marker.span = this; + if(this.refresh) this.refresh(); + + if(this.isEditorControlled()) { + let spanRange = this.spanRange(); + if(spanRange) { + this.editor.addToHistory(new SpanChange([spanRange], [], origin)); + } + } + } + + clear(origin = "+delete") { + if(!this.marker) return; + + let loc = this.find(); + if(this.isEditorControlled()) { + let spanRange = this.spanRange(); + if(spanRange) { + this.editor.addToHistory(new SpanChange([], [spanRange], origin)); + } + } + + this.marker.clear(); + this.marker = this.marker.span = undefined; + this.editor.queueUpdate(); + } + + find():Range|undefined { + if(!this.marker) return undefined; + let loc = this.marker.find(); + if(!loc) return; + if(isRange(loc)) return loc; + return {from: loc, to: loc}; + } + + spanRange():SpanRange|undefined { + let loc = this.find(); + if(!loc) return; + return {from: loc.from, to: loc.to, span: this}; + } + + hide() { + if(!this.hidden) { + this.hidden = true; + if(this.refresh) this.refresh(); + } + } + unhide() { + if(this.hidden) { + this.hidden = false; + if(this.refresh) this.refresh(); + } + } + + isHidden() { + return this.hidden; + } + + sourceEquals(other:SpanSource) { + return this.source.type = other.type; + } + + isInline(): this is InlineSpan { + return this._spanStyle == "inline"; + } + isLine(): this is LineSpan { + return this._spanStyle == "line"; + } + isBlock(): this is BlockSpan { + return this._spanStyle == "block"; + } + isEditorControlled() { + return this._editorControlled; + } + + static style() { + return this._spanStyle; + } +} + +// Optional life cycle methods for Span-derivatives.. +export interface Span { + refresh?(): void, + onBeforeChange?(change:ChangeCancellable): void + onChange?(change:Change): void + + normalize?(): void + isDenormalized?(): boolean +} + +export class InlineSpan extends Span { + static _spanStyle:"inline" = "inline"; + _spanStyle:"inline" = "inline"; + + apply(from:Position, to:Position, origin = "+input") { + if(samePosition(from, to)) throw new Error("Unable to create zero-width InlineSpan. Maybe you meant to use LineSpan?"); + super.apply(from, to, origin); + } + + // Handlers + onChange(change:Change) { + let loc = this.find(); + if(!loc) return; + let intersecting = this.editor.findSpansAt(loc.from); + for(let span of intersecting) { + // If the space between this span and a preceding inline span is removed + // delete this span and extend that one to contain it. + if(span.isInline() && span.isEditorControlled()) { + let otherLoc = span.find(); + if(!otherLoc) continue; + // If this is another span on the same word, ignore it. + if(samePosition(otherLoc.to, loc.to)) continue; + this.clear(); + span.clear(); + this.editor.markSpan(otherLoc.from, loc.to, span.source); + return; + } + } + + if(change.origin === "+input") { + let action = this.editor.formatting[this.type]; + formattingChange(this, change, action); + } + } + + isDenormalized() { + let loc = this.find(); + if(!loc) return; + let doc = this.editor.cm.getDoc(); + let fromLine = doc.getLine(loc.from.line); + let toLine = doc.getLine(loc.to.line); + + // Inline spans may not have internal leading or trailing whitespace. + if(loc.from.ch < fromLine.length && fromLine[loc.from.ch].search(/\s/) === 0) return true; + if(loc.to.ch - 1 < toLine.length && loc.to.ch - 1 >= 0 && toLine[loc.to.ch - 1].search(/\s/) === 0) return true; + } + + normalize() { + let loc = this.find(); + if(!loc) return this.clear(); + let doc = this.editor.cm.getDoc(); + let cur = doc.getRange(loc.from, loc.to); + + // Remove leading and trailing whitespace. + // Because trimLeft/Right aren't standard, we kludge a bit. + let adjustLeft = cur.length - (cur + "|").trim().length + 1; + let adjustRight = cur.length - ("|" + cur).trim().length + 1; + + let from = {line: loc.from.line, ch: loc.from.ch + adjustLeft}; + let to = {line: loc.to.line, ch: loc.to.ch - adjustRight}; + this.clear("+normalize"); + this.editor.markSpan(from, to, this.source); + } +} + +export class LineSpan extends Span { + static _spanStyle:"line" = "line"; + _spanStyle:"line" = "line"; + + lineTextClass?: string; + lineBackgroundClass?: string; + + apply(from:Position, to:Position, origin = "+input") { + if(!samePosition(from, to)) throw new Error("Unable to create non-zero-width LineSpan. Maybe you meant to use BlockSpan?"); + if(from.ch !== 0) throw new Error(`Unable to create LineSpan in middle of line at (${from.line}, ${from.ch})`); + super.apply(from, to, origin); + } + + clear(origin = "+delete") { + if(!this.marker) return; + + // If the line is still in the document, clear its classes. + let loc = this.find(); + if(loc) { + let end = loc.to.line + ((loc.from.line === loc.to.line) ? 1 : 0); + clearLineClasses(loc.from.line, end, this.editor, this); + } + super.clear(origin); + } + + // Handlers + refresh() { + let loc = this.find(); + if(!loc) return; + + let end = loc.to.line + ((loc.from.line === loc.to.line) ? 1 : 0); + if(!this.hidden) { + updateLineClasses(loc.from.line, end, this.editor, this); + } else { + clearLineClasses(loc.from.line, end, this.editor, this); + } + } + + onBeforeChange(change:ChangeCancellable) { + let loc = this.find(); + if(!loc) return; + let doc = this.editor.cm.getDoc(); + let isEmpty = doc.getLine(loc.from.line) === ""; + + //If we're at the beginning of an empty line and delete we mean to remove the span. + if(samePosition(loc.from, change.to) && isEmpty && change.origin === "+delete") { + this.clear(); + change.cancel(); + + // If we're at the beginning of line and delete into a non-empty line we remove the span too. + } else if(samePosition(loc.from, change.to) && + doc.getLine(change.from.line) !== "" && + change.origin === "+delete") { + this.clear(); + change.cancel(); + + // Similarly, if we're at the beginning of an empty line and hit enter + // we mean to remove the formatting. + } else if(samePosition(loc.from, change.from) && change.isNewlineChange() && isEmpty) { + this.clear(); + change.cancel(); + } + } + + onChange(change:Change) { + let loc = this.find(); + if(!loc) return; + + // If we're normalizing to put some space between the line and another span, make sure the span tracks its content. + if(change.origin === "+normalize" && samePosition(loc.from, change.from) && samePosition(loc.from, change.to)) { + this.editor.markSpan(change.final, change.final, this.source); + this.clear(); + } + } + + isDenormalized() { + // Line spans may not have leading or trailing whitespace. + let loc = this.find(); + if(!loc) return; + let doc = this.editor.cm.getDoc(); + let line = doc.getLine(loc.from.line); + if(!line) return; + if(line[0].search(/\s/) === 0 || line[line.length - 1].search(/\s/) === 0) return true; + } + + normalize() { + let loc = this.find(); + if(!loc) return this.clear(); + let doc = this.editor.cm.getDoc(); + + let to = doc.posFromIndex(doc.indexFromPos({line: loc.to.line + 1, ch: 0}) - 1); + let cur = doc.getRange(loc.from, to); + doc.replaceRange(cur.trim(), loc.from, to, "+normalize"); + } +} + +export class BlockSpan extends Span { + static _spanStyle:"block" = "block"; + _spanStyle:"block" = "block"; + + lineTextClass?: string; + lineBackgroundClass?: string; + + apply(from:Position, to:Position, origin = "+input") { + if(samePosition(from, to)) throw new Error("Unable to create zero-width BlockSpan. Maybe you meant to use LineSpan?"); + if(from.ch !== 0) throw new Error(`Unable to create BlockSpan starting in middle of line at (${from.line}, ${from.ch})`); + if(to.ch !== 0) throw new Error(`Unable to create BlockSpan ending in middle of line at (${to.line}, ${to.ch})`); + super.apply(from, to, origin); + } + + clear(origin = "+delete") { + if(!this.marker) return; + + // If the line is still in the document, clear its classes. + let loc = this.find(); + if(loc) { + clearLineClasses(loc.from.line, loc.to.line, this.editor, this); + } + super.clear(origin); + } + + refresh() { + let loc = this.find(); + if(!loc) return; + + if(!this.hidden) { + updateLineClasses(loc.from.line, loc.to.line, this.editor, this); + } else { + clearLineClasses(loc.from.line, loc.to.line, this.editor, this); + } + } + + onBeforeChange(change:ChangeCancellable) { + let loc = this.find(); + if(!loc) return; + let doc = this.editor.cm.getDoc(); + let isEmpty = doc.getLine(loc.from.line) === ""; + + //If we're at the beginning of an empty block and delete we mean to remove the span. + if(samePosition(loc.from, change.to) && isEmpty && change.origin === "+delete") { + this.clear(); + change.cancel(); + } + } + + onChange(change:Change) { + let loc = this.find(); + if(!loc) return; + + // Absorb local changes around a block. + let from = {line: loc.from.line, ch: 0}; + let to = {line: loc.to.line, ch: 0}; + if(loc.to.ch !== 0) { + to.line += 1; + } + + // If new text has been inserted left of the block, absorb it + // If the block's end has been removed, re-align it to the beginning of the next line. + if(comparePositions(change.final, change.to) >= 0) { + from.line = Math.min(loc.from.line, change.from.line); + to.line = Math.max(loc.to.line, change.to.line); + if(to.line === change.to.line && change.to.ch !== 0) { + to.line += 1; + } + } + + + if(!samePosition(from, loc.from) || !samePosition(to, loc.to)) { + this.clear(); + this.editor.markSpan(from, to, this.source); + } + } +} + +//--------------------------------------------------------- +// Special Spans +//--------------------------------------------------------- + +interface ListItemSpanSource extends SpanSource {level: number, listData: {start?: number, type:"ordered"|"bullet"}} +class ListItemSpan extends LineSpan { + source:ListItemSpanSource + bulletElem:HTMLElement; + + apply(from:Position, to:Position, origin = "+input") { + let source = this.source; + source.listData = source.listData || {type: "bullet"}; + source.level = source.level || 1; + + if(!this.bulletElem) { + this.bulletElem = document.createElement("span"); + } + this.bulletElem.style.paddingRight = ""+10; + this.bulletElem.style.paddingLeft = ""+(20 * (source.level - 1)); + this._attributes.widget = this.bulletElem; + + if(source.listData.type === "bullet") { + this.bulletElem.textContent = "-"; + } else { + this.bulletElem.textContent = `${source.listData.start !== undefined ? source.listData.start : 1}.`; + } + + this.lineTextClass = `ITEM ${this.source.listData.type} level-${this.source.level} start-${this.source.listData.start}`; + super.apply(from, to, origin); + } + + onChange(change:Change) { + let loc = this.find(); + if(!loc) return; + // If enter is pressed, continue the list + if(loc.from.line === change.from.line && change.isNewlineChange()) { + let next = change.final; + let src = this.source; + let ix = src.listData.start !== undefined ? src.listData.start + 1 : undefined; + let newSource = {type: src.type, level: src.level, listData: {type: src.listData.type, start: ix}}; + this.editor.markSpan(next, next, newSource); + } + } +} + +interface HeadingSpanSource extends SpanSource { level: number } +export class HeadingSpan extends LineSpan { + source:HeadingSpanSource; + + apply(from:Position, to:Position, origin = "+input") { + this.source.level = this.source.level || 1; + let cls = "HEADING" + this.source.level; + this.lineTextClass = cls; + this.lineBackgroundClass = cls; + + super.apply(from, to, origin); + this.editor.ide.navigator.updateNode(this); + } + + clear(origin = "+delete") { + super.clear(origin); + this.editor.ide.navigator.updateNode(this); + } + + refresh() { + super.refresh(); + this.editor.ide.navigator.updateNode(this); + } + + getSectionRange():Range|undefined { + let loc = this.find(); + if(!loc) return; + let from = {line: loc.from.line + 1, ch: 0}; + let to = {line: this.editor.cm.getDoc().lastLine() + 1, ch: 0}; + let headings = this.editor.findSpans(from, to, "heading") as HeadingSpan[]; + + if(headings.length) { + headings.sort(compareSpans); + let nextIx = 0; + let next = headings[nextIx++]; + while(next && next.source.level > this.source.level) { + next = headings[nextIx++]; + } + + if(next) { + let nextLoc = next.find(); + if(nextLoc) return {from: loc.from, to: nextLoc.from}; + } + } + + return {from: loc.from, to: {line: to.line - 1, ch: 0}}; + } +} + +class ElisionSpan extends BlockSpan { + protected element:HTMLElement; + + apply(from:Position, to:Position, origin = "+input") { + this.lineBackgroundClass = "elision"; + this.element = document.createElement("div"); + this.element.className = "elision-marker"; + this._attributes.replacedWith = this.element; + if(from.ch !== 0) from = {line: from.line, ch: 0}; + if(to.ch !== 0) to = {line: to.line, ch: 0}; + super.apply(from, to, origin); + + let doc = this.editor.cm.getDoc(); + + for(let span of this.editor.findSpansAt(from).concat(this.editor.findSpans(from, to))) { + if(span === this) continue; + span.hide(); + } + } + + clear(origin = "+delete") { + let loc = this.find(); + super.clear(origin); + if(loc) { + for(let span of this.editor.findSpansAt(loc.from).concat(this.editor.findSpans(loc.from, loc.to))) { + if(span === this) continue; + span.unhide(); + } + } + } +} + +interface CodeBlockSpanSource extends SpanSource { disabled?: boolean, info?: string } +export class CodeBlockSpan extends BlockSpan { + source: CodeBlockSpanSource; + protected disabled:boolean; + + protected widgetLine:number; + protected widget:CodeMirror.LineWidget; + protected widgetElem:HTMLElement; + protected enableToggleElem:HTMLElement; + + protected footerWidgetLine:number; + protected footerWidget:CodeMirror.LineWidget; + protected footerWidgetElem:HTMLElement; + + apply(from:Position, to:Position, origin = "+input") { + this.lineBackgroundClass = "code"; + this.lineTextClass = "code-text"; + if(this.source.disabled) this.disabled = this.source.disabled; + else this.disabled = false; + super.apply(from, to, origin); + + if(!this.widget) this.createWidgets(); + } + + clear(origin = "+delete") { + this.clearWidgets(); + + let loc = this.find(); + super.clear(origin); + + // Nuke all parser spans that were in this range. + // Since the parser isn't stateful, it won't send us removals for them. + if(loc) { + for(let span of this.editor.findSpans(loc.from, loc.to)) { + if(span.isEditorControlled()) continue; + span.clear(); + } + } + } + + refresh() { + super.refresh(); + this.updateWidgets(); + } + + disable() { + if(!this.disabled) { + this.source.info = "eve disabled"; + // @FIXME: We don't currently style this because of a bug in updateLineClasses. + // It's unable to intelligently remove unsupported classes, so we'd have to manually clear line classes. + // We can come back to this later if we care. + // this.lineBackgroundClass = "code code-disabled"; + // this.lineTextClass = "code-text code-disabled"; + this.disabled = true; + this.refresh(); + + this.editor.dirty = true; + this.editor.queueUpdate(true); + } + } + + enable() { + if(this.disabled) { + this.source.info = "eve"; + this.disabled = false; + this.refresh(); + + this.editor.dirty = true; + this.editor.queueUpdate(true); + } + } + + isDisabled() { + return this.disabled; + } + + createWidgets() { + if(this.widget) this.widget.clear(); + if(this.footerWidget) this.footerWidget.clear(); + + this.widgetElem = document.createElement("div"); + this.widgetElem.className = "code-controls-widget"; + + this.enableToggleElem = document.createElement("div"); + this.enableToggleElem.classList.add("enable-btn"); + this.enableToggleElem.onclick = () => { + if(this.disabled) + this.enable(); + else + this.disable(); + }; + this.widgetElem.appendChild(this.enableToggleElem); + + this.footerWidgetElem = document.createElement("div"); + this.footerWidgetElem.className = "code-footer-widget"; + + this.updateWidgets(); + } + + clearWidgets() { + this.widget.clear(); + this.footerWidget.clear(); + this.widget = this.widgetElem = this.widgetLine = undefined; + this.footerWidget = this.footerWidgetElem = this.footerWidgetLine = undefined; + } + + updateWidgets() { + if(!this.widgetElem) return; + + if(this.disabled) { + this.enableToggleElem.classList.remove("ion-android-checkbox-outline"); + this.enableToggleElem.classList.add("disabled", "ion-android-checkbox-outline-blank"); + } else { + this.enableToggleElem.classList.remove("disabled", "ion-android-checkbox-outline-blank"); + this.enableToggleElem.classList.add("ion-android-checkbox-outline"); + } + + let loc = this.find(); + if(loc) { + if(this.widgetLine !== loc.from.line) { + this.widgetLine = loc.from.line; + if(this.widget) this.widget.clear(); + this.widget = this.editor.cm.addLineWidget(this.widgetLine, this.widgetElem, {above: true}); + } + if(this.footerWidgetLine !== loc.to.line - 1) { + this.footerWidgetLine = loc.to.line - 1; + if(this.footerWidget) this.footerWidget.clear(); + this.footerWidget = this.editor.cm.addLineWidget(this.footerWidgetLine, this.footerWidgetElem); + } + } + } +} + +class WhitespaceSpan extends LineSpan { + normalize() { + super.normalize(); + this.clear(); + } +} + +export class BlockAnnotationSpan extends BlockSpan { + source:DocumentCommentSpanSource; + annotation?: CodeMirror.AnnotateScrollbar.Annotation; + + apply(from:Position, to:Position, origin = "+input") { + this.lineBackgroundClass = "annotated annotated_" + this.source.kind; + this._attributes.className = null; + super.apply(from, to, origin); + } + + clear(origin:string = "+delete") { + if(this.annotation) { + this.annotation.clear(); + this.annotation = undefined; + } + if(!this.marker) return; + let loc = this.find(); + if(loc) { + clearLineClasses(loc.from.line, loc.to.line, this.editor, this); + } + super.clear(origin); + } + + refresh() { + let loc = this.find(); + if(!loc) return this.clear(); + + if(!this.annotation) { + this.annotation = this.editor.cm.annotateScrollbar({className: `scrollbar-annotation ${this.source.kind}`}); + } + if(loc) { + this.annotation.update([loc]); + if(!this.hidden) { + updateLineClasses(loc.from.line, loc.to.line, this.editor, this); + } else { + clearLineClasses(loc.from.line, loc.to.line, this.editor, this); + } + } + } +} + +export class AnnotationSpan extends Span { + lineTextClass?:string + lineBackgroundClass?:string + + source:DocumentCommentSpanSource; + annotation?: CodeMirror.AnnotateScrollbar.Annotation; + + apply(from:Position, to:Position, origin = "+input") { + this.lineBackgroundClass = "annotated annotated_" + this.source.kind; + this._attributes.className = null; + super.apply(from, to, origin); + } + + clear(origin:string = "+delete") { + if(this.annotation) { + this.annotation.clear(); + this.annotation = undefined; + } + if(!this.marker) return; + let loc = this.find(); + if(loc) { + clearLineClasses(loc.from.line, loc.to.line, this.editor, this); + } + super.clear(origin); + } + + refresh() { + let loc = this.find(); + if(!loc) return this.clear(); + + if(!this.annotation) { + this.annotation = this.editor.cm.annotateScrollbar({className: `scrollbar-annotation ${this.source.kind}`}); + } + if(loc) { + this.annotation.update([loc]); + if(!this.hidden) { + updateLineClasses(loc.from.line, loc.to.line, this.editor, this); + } else { + clearLineClasses(loc.from.line, loc.to.line, this.editor, this); + } + } + } +} + +export class ParserSpan extends Span { + protected static _editorControlled = false; + protected _editorControlled = false; + static _spanStyle:"inline" = "inline"; + _spanStyle:"inline" = "inline"; +} + +interface DocumentCommentSpanSource extends SpanSource { kind: string, message: string, delay?: number } +export class DocumentCommentSpan extends ParserSpan { + source:DocumentCommentSpanSource; + + lineBackgroundClass: string; + annotation?: CodeMirror.AnnotateScrollbar.Annotation; + + widgetLine?: number; + commentWidget?: CodeMirror.LineWidget; + commentElem?: HTMLElement; + + apply(from:Position, to:Position, origin = "+input") { + this.lineBackgroundClass = "COMMENT_" + this.kind; + this._attributes.className = this.type + " " + this.kind; + + if(!this.commentElem) { + this.commentElem = document.createElement("div"); + } + + this.commentElem.className = "comment-widget" + " " + this.kind; + + if(this.editor.inCodeBlock(to)) { + this.commentElem.className += " code-comment-widget"; + } + + if(this.source.delay) { + this["updateWidget"] = debounce(this.updateWidget, this.source.delay); + } + super.apply(from, to, origin); + } + + clear(origin:string = "+delete") { + if(!this.marker) return; + + // If the line is still in the document, clear its classes. + let loc = this.find(); + if(loc) { + clearLineClasses(loc.from.line, loc.to.line, this.editor, this); + } + super.clear(origin); + if(this.annotation) { + this.annotation.clear(); + this.annotation = undefined; + } + + if(this.commentWidget) { + this.commentWidget.clear(); + this.commentElem.textContent = ""; + } + } + + refresh() { + let loc = this.find(); + if(!loc) return this.clear(); + + if(!this.annotation) { + this.annotation = this.editor.cm.annotateScrollbar({className: `scrollbar-annotation ${this.kind}`}); + } + if(loc) { + this.annotation.update([loc]); + if(!this.hidden) { + updateLineClasses(loc.from.line, loc.to.line, this.editor, this); + } else { + clearLineClasses(loc.from.line, loc.to.line, this.editor, this); + } + + if(loc.to.line !== this.widgetLine) { + this.widgetLine = loc.to.line; + if(this.commentWidget) this.commentWidget.clear(); + this.updateWidget(); + } + } + } + + updateWidget() { + if(this.commentWidget) this.commentWidget.clear(); + let loc = this.find(); + if(!loc) return; + this.widgetLine = loc.to.line; + this.commentElem.textContent = this.message; + this.commentWidget = this.editor.cm.addLineWidget(this.widgetLine, this.commentElem); + } + + get kind() { return this.source.kind || "error"; } + get message() { return this.source.message; } +} + +export class DocumentWidgetSpan extends ParserSpan { + source:DocumentCommentSpanSource; + + lineBackgroundClass: string; + + widgetLine?: number; + commentWidget?: CodeMirror.LineWidget; + commentElem?: HTMLElement; + + apply(from:Position, to:Position, origin = "+input") { + this.lineBackgroundClass = "COMMENT_" + this.kind; + this._attributes.className = this.type + " " + this.kind; + + if(!this.commentElem) { + this.commentElem = document.createElement("div"); + } + + this.commentElem.className = "comment-widget" + " " + this.kind; + + if(this.editor.inCodeBlock(to)) { + this.commentElem.className += " code-comment-widget"; + } + + if(this.source.delay) { + this["updateWidget"] = debounce(this.updateWidget, this.source.delay); + } + super.apply(from, to, origin); + } + + clear(origin:string = "+delete") { + if(!this.marker) return; + + // If the line is still in the document, clear its classes. + let loc = this.find(); + if(loc) { + clearLineClasses(loc.from.line, loc.to.line, this.editor, this); + } + super.clear(origin); + + if(this.commentWidget) { + this.commentWidget.clear(); + this.commentElem.textContent = ""; + } + } + + refresh() { + let loc = this.find(); + if(!loc) return this.clear(); + + if(loc) { + if(!this.hidden) { + updateLineClasses(loc.from.line, loc.to.line, this.editor, this); + } else { + clearLineClasses(loc.from.line, loc.to.line, this.editor, this); + } + + if(loc.to.line !== this.widgetLine) { + this.widgetLine = loc.to.line; + if(this.commentWidget) this.commentWidget.clear(); + this.updateWidget(); + } + } + } + + updateWidget() { + if(this.commentWidget) this.commentWidget.clear(); + let loc = this.find(); + if(!loc) return; + this.widgetLine = loc.to.line; + this.commentElem.textContent = this.message; + this.commentWidget = this.editor.cm.addLineWidget(this.widgetLine, this.commentElem); + } + + get kind() { return this.source.kind || "error"; } + get message() { return this.source.message; } +} + + +interface BadgeSpanSource extends SpanSource { kind: string, message: "string" } +class BadgeSpan extends ParserSpan { + source:BadgeSpanSource; + + badgeMarker:SpanMarker|undefined; + badgeElem:HTMLElement; + + apply(from:Position, to:Position, origin = "+input") { + this._attributes.className = `badge ${this.source.kind || ""}`; + if(!this.badgeElem) { + this.badgeElem = document.createElement("div"); + this.badgeElem.className = `badge-widget ${this.source.kind || ""}`; + } + + this.badgeElem.textContent = this.source.message; + + super.apply(from, to, origin); + + let doc = this.editor.cm.getDoc(); + this.badgeMarker = doc.setBookmark(to, {widget: this.badgeElem}); + this.badgeMarker.span = this; + } + + clear(origin = "+delete") { + super.clear(origin); + + if(this.badgeMarker) this.badgeMarker.clear(); + + if(this.badgeElem && this.badgeElem.parentNode) { + this.badgeElem.parentNode.removeChild(this.badgeElem); + } + this.badgeElem = undefined; + } +} + +interface LinkSpanSource extends SpanSource { destination?: string; } +class LinkSpan extends InlineSpan { + source:LinkSpanSource; + + linkWidget:HTMLAnchorElement; + bookmark:CodeMirror.TextMarker; + + apply(from:Position, to:Position, origin = "+input") { + if(this.bookmark) this.bookmark.clear(); + + this.linkWidget = document.createElement("a"); + this.linkWidget.className = "ion-android-open link-widget"; + this.linkWidget.target = "_blank"; + this.linkWidget.href = this.source.destination; + this.updateBookmark(); + + super.apply(from, to, origin); + } + + refresh() { + this.updateBookmark(); + } + + updateBookmark() { + let loc = this.find(); + if(!loc) return; + let to = {line: loc.to.line, ch: loc.to.ch + 1}; + + if(!this.bookmark) { + this.bookmark = this.editor.cm.getDoc().setBookmark(to, {widget: this.linkWidget}); + } else { + let bookmarkPos = this.bookmark.find() as Position; + if(!loc || !bookmarkPos) return; + if(!samePosition(bookmarkPos, to)) { + this.bookmark.clear(); + this.bookmark = this.editor.cm.getDoc().setBookmark(to, {widget: this.linkWidget}); + } + } + } + + clear(origin = "+delete") { + super.clear(origin); + if(this.bookmark) this.bookmark.clear(); + } +} + +//--------------------------------------------------------- +// Span Types +//--------------------------------------------------------- +export type InlineSpanType = "strong"|"emph"|"code"; +export type LineSpanType = "heading"|"item"|"elision"; +export type BlockSpanType = "code_block"; +export type SpanType = InlineSpanType|LineSpanType|BlockSpanType|"default"; + +export var spanTypes = { + whitespace: WhitespaceSpan, + strong: InlineSpan, + emph: InlineSpan, + code: InlineSpan, + link: LinkSpan, + + heading: HeadingSpan, + item: ListItemSpan, + elision: ElisionSpan, + elision_transient: ElisionSpan, + highlight: InlineSpan, + shadow: InlineSpan, + code_block: CodeBlockSpan, + + document_comment: DocumentCommentSpan, + document_widget: DocumentWidgetSpan, + annotation: AnnotationSpan, + block_annotation: BlockAnnotationSpan, + badge: BadgeSpan, + "default": ParserSpan +} + + +export interface SpanRange { + from: Position, + to: Position, + span: Span +} + +export class SpanChange { + type: string = "span"; + constructor(public added:SpanRange[] = [], public removed:SpanRange[] = [], public origin:string = "+input") {} + /** Inverts a change for undo. */ + invert() { return new SpanChange(this.removed, this.added, this.origin); } +} +export function isSpanChange(x:Change|SpanChange): x is SpanChange { + return x && x.type === "span"; +} diff --git a/src/microReact.js b/src/microReact.js new file mode 100644 index 000000000..38ee2168e --- /dev/null +++ b/src/microReact.js @@ -0,0 +1,576 @@ +function now() { + if (window.performance) { + return window.performance.now(); + } + return (new Date()).getTime(); +} +function shallowEquals(a, b) { + if (a === b) + return true; + if (!a || !b) + return false; + for (var k in a) { + if (a[k] !== b[k]) + return false; + } + for (var k in b) { + if (b[k] !== a[k]) + return false; + } + return true; +} +function postAnimationRemove(elements) { + for (var _i = 0; _i < elements.length; _i++) { + var elem = elements[_i]; + if (elem.parentNode) + elem.parentNode.removeChild(elem); + } +} +var Renderer = (function () { + function Renderer() { + this.content = document.createElement("div"); + this.content.className = "__root"; + this.elementCache = { "__root": this.content }; + this.prevTree = {}; + this.tree = {}; + this.postRenders = []; + this.lastDiff = { adds: [], updates: {} }; + var self = this; + this.handleEvent = function handleEvent(e) { + var id = (e.currentTarget || e.target)["_id"]; + var elem = self.tree[id]; + if (!elem) + return; + var handler = elem[e.type]; + if (handler) { + handler(e, elem); + } + }; + } + Renderer.compile = function (elem) { + if (!elem.id) + throw new Error("Cannot compile element with id " + elem.id); + var renderer = Renderer._compileRenderer[elem.id]; + if (!renderer) + renderer = Renderer._compileRenderer[elem.id] = new Renderer(); + renderer.render([elem]); + return renderer.elementCache[elem.id]; + }; + Renderer.prototype.reset = function () { + this.prevTree = this.tree; + this.tree = {}; + this.postRenders = []; + }; + Renderer.prototype.domify = function () { + var fakePrev = {}; //create an empty object once instead of every instance of the loop + var elements = this.tree; + var prevElements = this.prevTree; + var diff = this.lastDiff; + var adds = diff.adds; + var updates = diff.updates; + var elemKeys = Object.keys(updates); + var elementCache = this.elementCache; + var tempTween = {}; + //Create all the new elements to ensure that they're there when they need to be + //parented + for (var i = 0, len = adds.length; i < len; i++) { + var id = adds[i]; + var cur = elements[id]; + var div; + if (cur.svg) { + div = document.createElementNS("http://www.w3.org/2000/svg", cur.t || "rect"); + } + else { + div = document.createElement(cur.t || "div"); + } + div._id = id; + elementCache[id] = div; + if (cur.enter) { + if (cur.enter.delay) { + cur.enter.display = "auto"; + div.style.display = "none"; + } + Velocity(div, cur.enter, cur.enter); + } + } + for (var i = 0, len = elemKeys.length; i < len; i++) { + var id = elemKeys[i]; + var cur = elements[id]; + var prev = prevElements[id] || fakePrev; + var type = updates[id]; + var div; + if (type === "replaced") { + var me = elementCache[id]; + if (me.parentNode) + me.parentNode.removeChild(me); + if (cur.svg) { + div = document.createElementNS("http://www.w3.org/2000/svg", cur.t || "rect"); + } + else { + div = document.createElement(cur.t || "div"); + } + prev = fakePrev; + div._id = id; + elementCache[id] = div; + } + else if (type === "removed") { + //NOTE: Batching the removes such that you only remove the parent + //didn't actually make this faster surprisingly. Given that this + //strategy is much simpler and there's no noticable perf difference + //we'll just do the dumb thing and remove all the children one by one. + var me = elementCache[id]; + if (prev.leave) { + prev.leave.complete = postAnimationRemove; + if (prev.leave.absolute) { + me.style.position = "absolute"; + } + Velocity(me, prev.leave, prev.leave); + } + else if (me.parentNode) + me.parentNode.removeChild(me); + elementCache[id] = null; + continue; + } + else { + div = elementCache[id]; + } + var style = div.style; + if (cur.c !== prev.c) + div.className = cur.c; + if (cur.draggable !== prev.draggable) + div.draggable = cur.draggable === undefined ? null : "true"; + if (cur.contentEditable !== prev.contentEditable) + div.contentEditable = cur.contentEditable !== undefined ? JSON.stringify(cur.contentEditable) : "inherit"; + if (cur.colspan !== prev.colspan) + div.colSpan = cur.colspan; + if (cur.placeholder !== prev.placeholder) + div.setAttribute("placeholder", cur.placeholder); + if (cur.selected !== prev.selected) + div.selected = cur.selected; + if ((cur.value !== prev.value || cur.strictText) && div.value !== cur.value) + div.value = cur.value; + if (cur.t === "input" && cur.type !== prev.type) + div.type = cur.type; + if (cur.t === "input" && cur.checked !== prev.checked) + div.checked = cur.checked; + if ((cur.text !== prev.text || cur.strictText) && div.textContent !== cur.text) + div.textContent = cur.text === undefined ? "" : cur.text; + if (cur.tabindex !== prev.tabindex) + div.setAttribute("tabindex", cur.tabindex); + if (cur.title !== prev.title) + div.setAttribute("title", cur.title); + if (cur.href !== prev.href) + div.setAttribute("href", cur.href); + if (cur.src !== prev.src) + div.setAttribute("src", cur.src); + if (cur.target !== prev.target) + div.setAttribute("target", cur.target); + if (cur.data !== prev.data) + div.setAttribute("data", cur.data); + if (cur.download !== prev.download) + div.setAttribute("download", cur.download); + if (cur.allowfullscreen !== prev.allowfullscreen) + div.setAttribute("allowfullscreen", cur.allowfullscreen); + // animateable properties + var tween = cur.tween || tempTween; + if (cur.flex !== prev.flex) { + if (tween.flex) + tempTween.flex = cur.flex; + else + style.flex = cur.flex === undefined ? "" : cur.flex; + } + if (cur.left !== prev.left) { + if (tween.left) + tempTween.left = cur.left; + else + style.left = cur.left === undefined ? "" : cur.left; + } + if (cur.top !== prev.top) { + if (tween.top) + tempTween.top = cur.top; + else + style.top = cur.top === undefined ? "" : cur.top; + } + if (cur.height !== prev.height) { + if (tween.height) + tempTween.height = cur.height; + else + style.height = cur.height === undefined ? "auto" : cur.height; + } + if (cur.width !== prev.width) { + if (tween.width) + tempTween.width = cur.width; + else + style.width = cur.width === undefined ? "auto" : cur.width; + } + if (cur.zIndex !== prev.zIndex) { + if (tween.zIndex) + tempTween.zIndex = cur.zIndex; + else + style.zIndex = cur.zIndex; + } + if (cur.backgroundColor !== prev.backgroundColor) { + if (tween.backgroundColor) + tempTween.backgroundColor = cur.backgroundColor; + else + style.backgroundColor = cur.backgroundColor || "transparent"; + } + if (cur.borderColor !== prev.borderColor) { + if (tween.borderColor) + tempTween.borderColor = cur.borderColor; + else + style.borderColor = cur.borderColor || "none"; + } + if (cur.borderWidth !== prev.borderWidth) { + if (tween.borderWidth) + tempTween.borderWidth = cur.borderWidth; + else + style.borderWidth = cur.borderWidth || 0; + } + if (cur.borderRadius !== prev.borderRadius) { + if (tween.borderRadius) + tempTween.borderRadius = cur.borderRadius; + else + style.borderRadius = (cur.borderRadius || 0) + "px"; + } + if (cur.opacity !== prev.opacity) { + if (tween.opacity) + tempTween.opacity = cur.opacity; + else + style.opacity = cur.opacity === undefined ? 1 : cur.opacity; + } + if (cur.fontSize !== prev.fontSize) { + if (tween.fontSize) + tempTween.fontSize = cur.fontSize; + else + style.fontSize = cur.fontSize; + } + if (cur.color !== prev.color) { + if (tween.color) + tempTween.color = cur.color; + else + style.color = cur.color || "inherit"; + } + var animKeys = Object.keys(tempTween); + if (animKeys.length) { + Velocity(div, tempTween, tween); + tempTween = {}; + } + // non-animation style properties + if (cur.backgroundImage !== prev.backgroundImage) + style.backgroundImage = "url('" + cur.backgroundImage + "')"; + if (cur.border !== prev.border) + style.border = cur.border || "none"; + if (cur.textAlign !== prev.textAlign) { + style.alignItems = cur.textAlign; + if (cur.textAlign === "center") { + style.textAlign = "center"; + } + else if (cur.textAlign === "flex-end") { + style.textAlign = "right"; + } + else { + style.textAlign = "left"; + } + } + if (cur.verticalAlign !== prev.verticalAlign) + style.justifyContent = cur.verticalAlign; + if (cur.fontFamily !== prev.fontFamily) + style.fontFamily = cur.fontFamily || "inherit"; + if (cur.transform !== prev.transform) + style.transform = cur.transform || "none"; + if (cur.style !== prev.style) + div.setAttribute("style", cur.style); + if (cur.dangerouslySetInnerHTML !== prev.dangerouslySetInnerHTML) + div.innerHTML = cur.dangerouslySetInnerHTML; + // debug/programmatic properties + if (cur.semantic !== prev.semantic) + div.setAttribute("data-semantic", cur.semantic); + if (cur.debug !== prev.debug) + div.setAttribute("data-debug", cur.debug); + // SVG properties + if (cur.svg) { + if (cur.fill !== prev.fill) + div.setAttributeNS(null, "fill", cur.fill); + if (cur.stroke !== prev.stroke) + div.setAttributeNS(null, "stroke", cur.stroke); + if (cur.strokeWidth !== prev.strokeWidth) + div.setAttributeNS(null, "stroke-width", cur.strokeWidth); + if (cur.d !== prev.d) + div.setAttributeNS(null, "d", cur.d); + if (cur.c !== prev.c) + div.setAttributeNS(null, "class", cur.c); + if (cur.x !== prev.x) + div.setAttributeNS(null, "x", cur.x); + if (cur.y !== prev.y) + div.setAttributeNS(null, "y", cur.y); + if (cur.dx !== prev.dx) + div.setAttributeNS(null, "dx", cur.dx); + if (cur.dy !== prev.dy) + div.setAttributeNS(null, "dy", cur.dy); + if (cur.cx !== prev.cx) + div.setAttributeNS(null, "cx", cur.cx); + if (cur.cy !== prev.cy) + div.setAttributeNS(null, "cy", cur.cy); + if (cur.r !== prev.r) + div.setAttributeNS(null, "r", cur.r); + if (cur.height !== prev.height) + div.setAttributeNS(null, "height", cur.height); + if (cur.width !== prev.width) + div.setAttributeNS(null, "width", cur.width); + if (cur.xlinkhref !== prev.xlinkhref) + div.setAttributeNS('http://www.w3.org/1999/xlink', "href", cur.xlinkhref); + if (cur.startOffset !== prev.startOffset) + div.setAttributeNS(null, "startOffset", cur.startOffset); + if (cur.id !== prev.id) + div.setAttributeNS(null, "id", cur.id); + if (cur.viewBox !== prev.viewBox) + div.setAttributeNS(null, "viewBox", cur.viewBox); + if (cur.transform !== prev.transform) + div.setAttributeNS(null, "transform", cur.transform); + if (cur.draggable !== prev.draggable) + div.setAttributeNS(null, "draggable", cur.draggable); + if (cur.textAnchor !== prev.textAnchor) + div.setAttributeNS(null, "text-anchor", cur.textAnchor); + } + //events + if (cur.dblclick !== prev.dblclick) + div.ondblclick = cur.dblclick !== undefined ? this.handleEvent : undefined; + if (cur.click !== prev.click) + div.onclick = cur.click !== undefined ? this.handleEvent : undefined; + if (cur.contextmenu !== prev.contextmenu) + div.oncontextmenu = cur.contextmenu !== undefined ? this.handleEvent : undefined; + if (cur.mousedown !== prev.mousedown) + div.onmousedown = cur.mousedown !== undefined ? this.handleEvent : undefined; + if (cur.mousemove !== prev.mousemove) + div.onmousemove = cur.mousemove !== undefined ? this.handleEvent : undefined; + if (cur.mouseup !== prev.mouseup) + div.onmouseup = cur.mouseup !== undefined ? this.handleEvent : undefined; + if (cur.mouseover !== prev.mouseover) + div.onmouseover = cur.mouseover !== undefined ? this.handleEvent : undefined; + if (cur.mouseout !== prev.mouseout) + div.onmouseout = cur.mouseout !== undefined ? this.handleEvent : undefined; + if (cur.mouseleave !== prev.mouseleave) + div.onmouseleave = cur.mouseleave !== undefined ? this.handleEvent : undefined; + if (cur.mousewheel !== prev.mousewheel) + div.onmouseheel = cur.mousewheel !== undefined ? this.handleEvent : undefined; + if (cur.dragover !== prev.dragover) + div.ondragover = cur.dragover !== undefined ? this.handleEvent : undefined; + if (cur.dragstart !== prev.dragstart) + div.ondragstart = cur.dragstart !== undefined ? this.handleEvent : undefined; + if (cur.dragend !== prev.dragend) + div.ondragend = cur.dragend !== undefined ? this.handleEvent : undefined; + if (cur.drag !== prev.drag) + div.ondrag = cur.drag !== undefined ? this.handleEvent : undefined; + if (cur.drop !== prev.drop) + div.ondrop = cur.drop !== undefined ? this.handleEvent : undefined; + if (cur.scroll !== prev.scroll) + div.onscroll = cur.scroll !== undefined ? this.handleEvent : undefined; + if (cur.focus !== prev.focus) + div.onfocus = cur.focus !== undefined ? this.handleEvent : undefined; + if (cur.blur !== prev.blur) + div.onblur = cur.blur !== undefined ? this.handleEvent : undefined; + if (cur.input !== prev.input) + div.oninput = cur.input !== undefined ? this.handleEvent : undefined; + if (cur.change !== prev.change) + div.onchange = cur.change !== undefined ? this.handleEvent : undefined; + if (cur.keyup !== prev.keyup) + div.onkeyup = cur.keyup !== undefined ? this.handleEvent : undefined; + if (cur.keydown !== prev.keydown) + div.onkeydown = cur.keydown !== undefined ? this.handleEvent : undefined; + if (cur.cut !== prev.cut) + div.oncut = cur.cut !== undefined ? this.handleEvent : undefined; + if (cur.copy !== prev.copy) + div.oncopy = cur.copy !== undefined ? this.handleEvent : undefined; + if (cur.paste !== prev.paste) + div.onpaste = cur.paste !== undefined ? this.handleEvent : undefined; + if (type === "added" || type === "replaced" || type === "moved") { + var parentEl = elementCache[cur.parent]; + if (parentEl) { + if (cur.ix >= parentEl.children.length) { + parentEl.appendChild(div); + } + else { + parentEl.insertBefore(div, parentEl.children[cur.ix]); + } + } + } + } + }; + Renderer.prototype.diff = function () { + var a = this.prevTree; + var b = this.tree; + var as = Object.keys(a); + var bs = Object.keys(b); + var updated = {}; + var adds = []; + for (var i = 0, len = as.length; i < len; i++) { + var id = as[i]; + var curA = a[id]; + var curB = b[id]; + if (curB === undefined) { + updated[id] = "removed"; + continue; + } + if (curA.t !== curB.t) { + updated[id] = "replaced"; + continue; + } + if (curA.ix !== curB.ix || curA.parent !== curB.parent) { + updated[id] = "moved"; + continue; + } + if (!curB.dirty + && curA.c === curB.c + && curA.key === curB.key + && curA.dangerouslySetInnerHTML === curB.dangerouslySetInnerHTML + && curA.tabindex === curB.tabindex + && curA.title === curB.title + && curA.href === curB.href + && curA.src === curB.src + && curA.data === curB.data + && curA.download === curB.download + && curA.allowfullscreen === curB.allowfullscreen + && curA.placeholder === curB.placeholder + && curA.selected === curB.selected + && curA.draggable === curB.draggable + && curA.contentEditable === curB.contentEditable + && curA.value === curB.value + && curA.target === curB.target + && curA.type === curB.type + && curA.checked === curB.checked + && curA.text === curB.text + && curA.top === curB.top + && curA.flex === curB.flex + && curA.left === curB.left + && curA.width === curB.width + && curA.height === curB.height + && curA.zIndex === curB.zIndex + && curA.backgroundColor === curB.backgroundColor + && curA.backgroundImage === curB.backgroundImage + && curA.color === curB.color + && curA.colspan === curB.colspan + && curA.border === curB.border + && curA.borderColor === curB.borderColor + && curA.borderWidth === curB.borderWidth + && curA.borderRadius === curB.borderRadius + && curA.opacity === curB.opacity + && curA.fontFamily === curB.fontFamily + && curA.fontSize === curB.fontSize + && curA.textAlign === curB.textAlign + && curA.transform === curB.transform + && curA.verticalAlign === curB.verticalAlign + && curA.semantic === curB.semantic + && curA.debug === curB.debug + && curA.style === curB.style + && (curB.svg === undefined || (curA.x === curB.x + && curA.y === curB.y + && curA.dx === curB.dx + && curA.dy === curB.dy + && curA.cx === curB.cx + && curA.cy === curB.cy + && curA.r === curB.r + && curA.d === curB.d + && curA.fill === curB.fill + && curA.stroke === curB.stroke + && curA.strokeWidth === curB.strokeWidth + && curA.startOffset === curB.startOffset + && curA.textAnchor === curB.textAnchor + && curA.viewBox === curB.viewBox + && curA.xlinkhref === curB.xlinkhref))) { + continue; + } + updated[id] = "updated"; + } + for (var i = 0, len = bs.length; i < len; i++) { + var id = bs[i]; + var curA = a[id]; + if (curA === undefined) { + adds.push(id); + updated[id] = "added"; + continue; + } + } + this.lastDiff = { adds: adds, updates: updated }; + return this.lastDiff; + }; + Renderer.prototype.prepare = function (root) { + var elemLen = 1; + var tree = this.tree; + var elements = [root]; + var elem; + for (var elemIx = 0; elemIx < elemLen; elemIx++) { + elem = elements[elemIx]; + if (elem.parent === undefined) + elem.parent = "__root"; + if (elem.id === undefined) + elem.id = "__root__" + elemIx; + tree[elem.id] = elem; + if (elem.postRender !== undefined) { + this.postRenders.push(elem); + } + var children = elem.children; + if (children !== undefined) { + for (var childIx = 0, len = children.length; childIx < len; childIx++) { + var child = children[childIx]; + if (child === undefined) + continue; + if (child.id === undefined) { + child.id = elem.id + "__" + childIx; + } + if (child.ix === undefined) { + child.ix = childIx; + } + if (child.parent === undefined) { + child.parent = elem.id; + } + elements.push(child); + elemLen++; + } + } + } + return tree; + }; + Renderer.prototype.postDomify = function () { + var postRenders = this.postRenders; + var diff = this.lastDiff.updates; + var elementCache = this.elementCache; + for (var i = 0, len = postRenders.length; i < len; i++) { + var elem = postRenders[i]; + var id = elem.id; + if (diff[id] === "updated" || diff[id] === "added" || diff[id] === "replaced" || elem.dirty || diff[id] === "moved") { + elem.postRender(elementCache[elem.id], elem); + } + } + }; + Renderer.prototype.render = function (elems) { + this.reset(); + // We sort elements by depth to allow them to be self referential. + elems.sort(function (a, b) { return (a.parent ? a.parent.split("__").length : 0) - (b.parent ? b.parent.split("__").length : 0); }); + var start = now(); + for (var _i = 0; _i < elems.length; _i++) { + var elem = elems[_i]; + var post = this.prepare(elem); + } + var prepare = now(); + var d = this.diff(); + var diff = now(); + this.domify(); + var domify = now(); + this.postDomify(); + var postDomify = now(); + var time = now() - start; + if (time > 5) { + console.log("slow render (> 5ms): ", time, { + prepare: prepare - start, + diff: diff - prepare, + domify: domify - diff, + postDomify: postDomify - domify + }); + } + }; + // @TODO: A more performant implementation would have a way of rendering subtrees and just have a lambda Renderer to compile into + Renderer._compileRenderer = {}; + return Renderer; +})(); + diff --git a/src/renderer.ts b/src/renderer.ts new file mode 100644 index 000000000..0be4149e8 --- /dev/null +++ b/src/renderer.ts @@ -0,0 +1,741 @@ +"use strict" + +import {Renderer} from "microReact"; +import {clone} from "./util"; +import {client, indexes} from "./client"; + +//type RecordElementCollection = HTMLCollection | SVGColl +interface RecordElement extends Element { entity?: string, sort?: any, _parent?: RecordElement|null, style?: CSSStyleDeclaration }; +interface RDocument extends Document { activeElement: RecordElement }; +declare var document: RDocument; + +function isInputElem(elem:T): elem is T&HTMLInputElement { + return elem && elem.tagName === "INPUT"; +} +function isSelectElem(elem:T): elem is T&HTMLSelectElement { + return elem && elem.tagName === "SELECT"; +} + +export function setActiveIds(ids) { + for(let k in activeIds) { + activeIds[k] = undefined; + } + for(let k in ids) { + activeIds[k] = ids[k]; + } +} + + +//--------------------------------------------------------- +// MicroReact-based record renderer +//--------------------------------------------------------- +export var renderer = new Renderer(); +document.body.appendChild(renderer.content); +renderer.content.classList.add("application-root"); + +// These will get maintained by the client as diffs roll in +export var sentInputValues = {}; +export var activeIds = {}; + +// root will get added to the dom by the program microReact element in renderEditor +export var activeElements:{[id : string]: RecordElement|null, root: RecordElement} = {"root": document.createElement("div")}; +activeElements.root.className = "program"; + +// Obtained from http://w3c.github.io/html-reference/elements.html +var supportedTagsArr = [ + "a", + "abbr", + "address", + "area", + "article", + "aside", + "audio", + "b", + "base", + "bdi", + "bdo", + "blockquote", + "body", + "br", + "button", + "canvas", + "caption", + "cite", + "code", + "col", + "colgroup", + "command", + "datalist", + "dd", + "del", + "details", + "dfn", + "div", + "dl", + "dt", + "em", + "embed", + "fieldset", + "figcaption", + "figure", + "footer", + "form", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "head", + "header", + "hgroup", + "hr", + "html", + "i", + "iframe", + "img", + "input", + "ins", + "kbd", + "keygen", + "label", + "legend", + "li", + "link", + "map", + "mark", + "menu", + "meta", + "meter", + "nav", + "noscript", + "object", + "ol", + "optgroup", + "option", + "output", + "p", + "param", + "pre", + "progress", + "q", + "rp", + "rt", + "ruby", + "s", + "samp", + "script", + "section", + "select", + "small", + "source", + "span", + "strong", + "style", + "sub", + "summary", + "sup", + "table", + "tbody", + "td", + "textarea", + "tfoot", + "th", + "thead", + "time", + "title", + "tr", + "track", + "u", + "ul", + "var", + "video", + "wbr" +]; + +// Obtained from https://www.w3.org/TR/SVG/eltindex.html +var svgsArr = [ + "a", + "altGlyph", + "altGlyphDef", + "altGlyphItem", + "animate", + "animateColor", + "animateMotion", + "animateTransform", + "circle", + "clipPath", + "color-profile", + "cursor", + "defs", + "desc", + "ellipse", + "feBlend", + "feColorMatrix", + "feComponentTransfer", + "feComposite", + "feConvolveMatrix", + "feDiffuseLighting", + "feDisplacementMap", + "feDistantLight", + "feFlood", + "feFuncA", + "feFuncB", + "feFuncG", + "feFuncR", + "feGaussianBlur", + "feImage", + "feMerge", + "feMergeNode", + "feMorphology", + "feOffset", + "fePointLight", + "feSpecularLighting", + "feSpotLight", + "feTile", + "feTurbulence", + "filter", + "font", + "font-face", + "font-face-format", + "font-face-name", + "font-face-src", + "font-face-uri", + "foreignObject", + "g", + "glyph", + "glyphRef", + "hkern", + "image", + "line", + "linearGradient", + "marker", + "mask", + "metadata", + "missing-glyph", + "mpath", + "path", + "pattern", + "polygon", + "polyline", + "radialGradient", + "rect", + "script", + "set", + "stop", + "style", + "svg", + "switch", + "symbol", + "text", + "textPath", + "title", + "tref", + "tspan", + "use", + "view", + "vkern" +]; + +supportedTagsArr.push.apply(supportedTagsArr, svgsArr); + +function toKeys(arr) { + var obj = {}; + for (var el of arr) { + obj[el] = true; + } + return obj +} + +var supportedTags = toKeys(supportedTagsArr); +var svgs = toKeys(svgsArr); + +// Map of input entities to a queue of their values which originated from the client and have not been received from the server yet. +var lastFocusPath:string[]|null = null; +var selectableTypes = {"": true, undefined: true, text: true, search: true, password: true, tel: true, url: true}; + +var previousCheckedRadios = {}; + +function insertSorted(parent:Node, child:RecordElement) { + let current; + for(let curIx = 0; curIx < parent.childNodes.length; curIx++) { + let cur = parent.childNodes[curIx] as RecordElement; + if(cur.sort !== undefined && cur.sort > child.sort) { + current = cur; + break; + } + } + if(current) { + parent.insertBefore(child, current); + } else { + parent.appendChild(child); + } +} + +let _suppressBlur = false; // This global is set when the records are being re-rendered, to prevent false blurs from mucking up focus tracking. + +export function renderRecords() { + _suppressBlur = true; + let lastActiveElement:RecordElement|null = null; + if(document.activeElement && document.activeElement.entity) { + lastActiveElement = document.activeElement; + } + + let records = indexes.records.index; + let dirty = indexes.dirty.index; + let activeClasses = indexes.byClass.index || {}; + let activeStyles = indexes.byStyle.index || {}; + let activeChildren = indexes.byChild.index || {}; + + let regenClassesFor:string[] = []; + let regenStylesFor:string[] = []; + + for(let entityId in dirty) { + let entity = records[entityId]; + let elem:RecordElement|null = activeElements[entityId]; + + if(dirty[entityId].indexOf("tag") !== -1) { + let values = entity.tag || [] + let tag; + for(let val of values) { + if(supportedTags[val]) { + if(tag) console.error("Unable to set 'tag' multiple times on entity", entity, entity.tag); + tag = val; + } + } + + if(!tag && elem && elem !== activeElements.root) { // Nuke the element if it no longer has a supported tag + let parent = elem.parentNode; + if(parent) parent.removeChild(elem); + elem = activeElements[entityId] = null; + + } else if(tag && elem && elem.tagName !== tag.toUpperCase()) { // Nuke and restore the element if its tag has changed + let parent = elem.parentNode; + if(parent) parent.removeChild(elem); + if(svgs[tag]) { + elem = document.createElementNS("http://www.w3.org/2000/svg", tag) as RecordElement; + } else { + elem = document.createElement(tag || "div") + } + // Mark all attributes of the entity dirty to rerender them into the new element + for(let attribute in entity) { + if(dirty[entityId].indexOf(attribute) == -1) { + dirty[entityId].push(attribute); + } + } + elem.entity = entityId; + activeElements[entityId] = elem; + if(entity.sort && entity.sort.length > 1) console.error("Unable to set 'sort' multiple times on entity", entity, entity.sort); + if(entity.sort !== undefined && entity.sort[0] !== undefined) { + elem.sort = entity.sort[0]; + } else if(entity["eve-auto-index"] !== undefined && entity["eve-auto-index"][0] !== undefined) { + elem.sort = entity["eve-auto-index"][0]; + } else { + elem.sort = ""; + } + if(parent) insertSorted(parent, elem) + + + } else if(tag && !elem) { // Create a new element and mark all its attributes dirty to restore it. + if(svgs[tag]) { + elem = document.createElementNS("http://www.w3.org/2000/svg", tag); + } else { + elem = document.createElement(tag || "div") + } + elem.entity = entityId; + activeElements[entityId] = elem; + if(entity.sort && entity.sort.length > 1) console.error("Unable to set 'sort' multiple times on entity", entity, entity.sort); + if(entity.sort !== undefined && entity.sort[0] !== undefined) { + elem.sort = entity.sort[0]; + } else if(entity["eve-auto-index"] !== undefined && entity["eve-auto-index"][0] !== undefined) { + elem.sort = entity["eve-auto-index"][0]; + } else { + elem.sort = ""; + } + let parent = activeElements[activeChildren[entityId] || "root"]; + if(parent) insertSorted(parent, elem); + } + } + + if(activeClasses[entityId]) { + for(let entId of activeClasses[entityId]) { + regenClassesFor.push(entId); + } + } else if(activeStyles[entityId]) { + for(let entId of activeStyles[entityId]) { + regenStylesFor.push(entId); + } + } + + if(!elem) continue; + + for(let attribute of dirty[entityId]) { + let value = entity[attribute]; + + if(attribute === "children") { + if(!value) { // Remove all children + while(elem.lastElementChild) { + elem.removeChild(elem.lastElementChild); + } + } else { + let children = (value && clone(value)) || []; + // Remove any children that no longer belong + for(let ix = elem.childNodes.length - 1; ix >= 0; ix--) { + if(!(elem.childNodes[ix] instanceof Element)) continue; + let child = elem.childNodes[ix] as RecordElement; + let childIx = children.indexOf(child.entity); + if(childIx == -1) { + elem.removeChild(child); + child._parent = null; + } else { + children.splice(childIx, 1); + } + } + // Add any new children which already exist + for(let childId of children) { + let child = activeElements[childId]; + if(child) { + insertSorted(elem, child); + } + } + } + } else if(attribute === "class") { + regenClassesFor.push(entityId); + + } else if(attribute === "style") { + regenStylesFor.push(entityId); + + } else if(attribute === "text") { + elem.textContent = (value && value.join(", ")) || ""; + + } else if(attribute === "value") { + let input = elem as (RecordElement & HTMLInputElement); + if(!value) { + input.value = ""; + } else if(value.length > 1) { + console.error("Unable to set 'value' multiple times on entity", entity, JSON.stringify(value)); + } else { + input.setAttribute('value', value[0]); + } + + } else if(attribute === "checked") { + if(value && value.length > 1) { + console.error("Unable to set 'checked' multiple times on entity", entity, value); + } else if(value && value[0]) { + elem.setAttribute("checked", "true"); + if (elem.getAttribute("type") == "radio") { + var name = elem.getAttribute("name") || ""; + previousCheckedRadios[name] = entityId; + } + } else { + elem.removeAttribute("checked"); + } + + } else { + value = value && value.join(", "); + if(value === undefined) { + elem.removeAttribute(attribute); + } else { + elem.setAttribute(attribute, value); + } + } + } + + let attrs = Object.keys(entity); + } + + for(let entityId of regenClassesFor) { + let elem = activeElements[entityId]; + if(!elem) continue; + let entity = records[entityId]; + let value = entity["class"]; + if(!value) { + elem.className = ""; + } else { + let neue:string[] = []; + for(let klassId of value) { + if(activeClasses[klassId] !== undefined && records[klassId] !== undefined) { + let klass = records[klassId]; + for(let name in klass) { + if(!klass[name]) continue; + if(klass[name].length > 1) { + console.error("Unable to set class attribute to multiple values on entity", entity, name, klass[name]); + continue; + } + if(klass[name][0] && neue.indexOf(name) === -1) { + neue.push(name); + } + } + } else { + neue.push(klassId); + } + } + elem.className = neue.join(" "); + } + } + + for(let entityId of regenStylesFor) { + let elem = activeElements[entityId]; + if(!elem) continue; + let entity = records[entityId]; + let value = entity["style"]; + elem.removeAttribute("style"); // @FIXME: This could be optimized to care about the diff rather than blowing it all away + if(value) { + let neue:string[] = []; + for(let styleId of value) { + if(activeStyles[styleId]) { + let style = records[styleId]; + for(let attr in style) { + (elem as any).style[attr] = style[attr] && style[attr].join(", "); + } + } else { + neue.push(styleId); + } + } + if(neue.length) { + let s = elem.getAttribute("style"); + elem.setAttribute("style", (s ? (s + "; ") : "") + neue.join("; ")); + } + } + } + + if(lastFocusPath && lastActiveElement && isInputElem(lastActiveElement)) { + let current = activeElements.root; + let ix = 0; + for(let segment of lastFocusPath) { + current = current.childNodes[segment] as RecordElement; + if(!current) { + lastActiveElement.blur(); + lastFocusPath = null; + break; + } + ix++; + } + if(current && current.entity !== lastActiveElement.entity) { + let curElem = current as HTMLElement; + curElem.focus(); + if(isInputElem(lastActiveElement) && isInputElem(current) && selectableTypes[lastActiveElement.type] && selectableTypes[current.type]) { + current.setSelectionRange(lastActiveElement.selectionStart, lastActiveElement.selectionEnd); + } + } + } + _suppressBlur = false +} + +//--------------------------------------------------------- +// Event bindings to forward events to the server +//--------------------------------------------------------- + +function addSVGCoods(elem, event, eveEvent) { + if(elem.tagName != "svg") return; + + var pt = elem.createSVGPoint(); + pt.x = event.clientX; + pt.y = event.clientY; + let coords = pt.matrixTransform(elem.getScreenCTM().inverse()); + eveEvent.x = coords.x; + eveEvent.y = coords.y; +} + +function addRootEvent(elem, event, objs) { + if(elem !== activeElements["root"]) return; + + let eveEvent = { + tag: objs.length === 0 ? ["click"] : ["click", "direct-target"], + root: true, + x: event.clientX, + y: event.clientY + }; + objs.push(eveEvent); +} + +window.addEventListener("click", function(event) { + let {target} = event; + let current = target as RecordElement; + let objs:any[] = []; + while(current) { + if(current.entity) { + let tag = ["click"]; + if(current == target) { + tag.push("direct-target"); + } + let eveEvent = {tag, element: current.entity}; + addSVGCoods(current, event, eveEvent) + objs.push(eveEvent); + } + addRootEvent(current, event, objs); + current = current.parentElement; + } + client.sendEvent(objs); +}); +window.addEventListener("dblclick", function(event) { + let {target} = event; + let current = target as RecordElement; + let objs:any[] = []; + while(current) { + if(current.entity) { + let tag = ["double-click"]; + if(current == target) { + tag.push("direct-target"); + } + let eveEvent = {tag, element: current.entity}; + addSVGCoods(current, event, eveEvent) + objs.push(eveEvent); + } + addRootEvent(current, event, objs); + current = current.parentElement; + } + client.sendEvent(objs); +}); + +window.addEventListener("input", function(event) { + let target = event.target as (RecordElement & HTMLInputElement); + if(target.entity) { + if(!sentInputValues[target.entity]) { + sentInputValues[target.entity] = []; + } + sentInputValues[target.entity].push(target.value); + client.sendEvent([{tag: ["change"], element: target.entity, value: target.value}]); + } +}); +window.addEventListener("change", function(event) { + let target = event.target as (RecordElement & (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement)); + if(target.tagName == "TEXTAREA") return; + if(target.tagName == "INPUT") { + let type = target.getAttribute("type"); + if(type != "checkbox" && type != "radio") return; + let tickbox = target as (RecordElement & HTMLInputElement); + if(!tickbox.entity) return; + client.sendEvent([{tag: ["change", "direct-target"], element: tickbox.entity, checked: tickbox.checked}]); + if(type == "radio") { + var name = target.getAttribute("name") || ""; + if(name in previousCheckedRadios) { + var previousEntity = previousCheckedRadios[name]; + client.sendEvent([{tag: ["change"], element: previousEntity, checked: false}]); + } + } + } else if(target.entity) { + if(!sentInputValues[target.entity]) { + sentInputValues[target.entity] = []; + } + let value = target.value; + + if(isSelectElem(target)) { + value = target.options[target.selectedIndex].value; + } + + sentInputValues[target.entity!].push(value); + let tag = ["change"]; + if(target == target) { + tag.push("direct-target"); + } + client.sendEvent([{tag, element: target.entity, value: target.value}]); + } +}); + +function getFocusPath(target) { + let root = activeElements.root; + let current = target; + let path:string[] = []; + while(current !== root && current && current.parentElement) { + let parent = current.parentElement; + path.unshift(Array.prototype.indexOf.call(parent.children, current)); + current = parent; + } + return path; +} + +window.addEventListener("focus", function(event) { + let target = event.target as RecordElement; + if(target.entity) { + let objs = [{tag: ["focus"], element: target.entity}]; + client.sendEvent(objs); + lastFocusPath = getFocusPath(target); + } +}, true); + +window.addEventListener("blur", function(event) { + if(_suppressBlur) { + event.preventDefault(); + return; + } + let target = event.target as RecordElement; + if(target.entity) { + let objs = [{tag: ["blur"], element: target.entity}]; + client.sendEvent(objs); + + if(lastFocusPath) { + let curFocusPath = getFocusPath(target); + if(curFocusPath.length === lastFocusPath.length) { + let match = true; + for(let ix = 0; ix < curFocusPath.length; ix++) { + if(curFocusPath[ix] !== lastFocusPath[ix]) { + match = false; + break; + } + } + if(match) { + lastFocusPath = null; + } + } + } + } +}, true); + + +let keyMap = {13: "enter", 27: "escape"} +window.addEventListener("keydown", function(event) { + let {target} = event; + let current = target as RecordElement; + let objs:any[] = []; + let key = event.keyCode; + while(current) { + if(current.entity) { + let tag = ["keydown"]; + if (current == target) { + tag.push("direct-target"); + } + objs.push({tag, element: current.entity, key: keyMap[key] || key}); + } + current = current.parentElement; + } + objs.push({tag: ["keydown"], element: "window", key}); + client.sendEvent(objs); +}); + +window.addEventListener("keyup", function(event) { + let {target} = event; + let current = target as RecordElement; + let objs:any[] = []; + let key = event.keyCode; + while(current) { + if(current.entity) { + let tag = ["keyup"]; + if (current == target) { + tag.push("direct-target"); + } + objs.push({tag, element: current.entity, key: keyMap[key] || key}); + } + current = current.parentElement; + } + objs.push({tag: ["keyup"], element: "window", key}); + client.sendEvent(objs); +}); + + +//--------------------------------------------------------- +// Editor Renderer +//--------------------------------------------------------- +let activeLayers = {}; +let editorParse = {}; +let allNodeGraphs = {}; +let showGraphs = false; + +function injectProgram(node, elem) { + node.appendChild(activeElements.root); +} + +export function renderEve() { + renderer.render([{c: "application-container", postRender: injectProgram}]); +} + diff --git a/src/runtime/actions.ts b/src/runtime/actions.ts new file mode 100644 index 000000000..791a6d26b --- /dev/null +++ b/src/runtime/actions.ts @@ -0,0 +1,150 @@ +//--------------------------------------------------------------------- +// Actions +//--------------------------------------------------------------------- + +import {Variable, isVariable, toValue} from "./join"; +import {MultiIndex} from "./indexes"; +import {Changes} from "./changes"; + +//--------------------------------------------------------------------- +// Actions +//--------------------------------------------------------------------- + +export abstract class Action { + id: string; + e: any; + a: any; + v: any; + node: string; + vars: Variable[]; + resolved: any[]; + scopes: string[]; + constructor(id: string, e,a,v,node?,scopes?) { + this.id = id; + this.resolved = []; + let eav = [e,a,v]; + this.e = e; + this.a = a; + this.v = v; + this.node = node || this.id; + this.vars = []; + this.scopes = scopes || ["session"]; + for(let register of eav) { + if(isVariable(register)) { + this.vars[register.id] = register; + } + } + } + + // Return an array of the current values for all the registers + resolve(prefix) { + let resolved = this.resolved; + resolved[0] = toValue(this.e, prefix); + resolved[1] = toValue(this.a, prefix); + resolved[2] = toValue(this.v, prefix); + return resolved; + } + + abstract execute(multiIndex: MultiIndex, row: any, changes: Changes); +} + +export class InsertAction extends Action { + execute(multiIndex, row, changes) { + let [e,a,v] = this.resolve(row); + for(let scope of this.scopes) { + changes.store(scope,e,a,v,this.node); + } + } +} + +export class RemoveAction extends Action { + execute(multiIndex, row, changes) { + let [e,a,v] = this.resolve(row); + for(let scope of this.scopes) { + changes.unstore(scope,e,a,v); + } + } +} + +export class RemoveSupportAction extends Action { + execute(multiIndex, row, changes) { + let [e,a,v] = this.resolve(row); + // console.log("removing support for", e,a,v, this.node); + for(let scope of this.scopes) { + changes.unstore(scope,e,a,v,this.node); + } + } +} + +export class EraseAction extends Action { + removeVs(index, changes, scope, e, a) { + let keys = Object.keys(index); + for(let key of keys) { + let value = index[key].value; + changes.unstore(scope,e,a,value); + } + } + execute(multiIndex, row, changes) { + let [e,a] = this.resolve(row); + // multidb + for(let scope of this.scopes) { + let avIndex = multiIndex.getIndex(scope).lookup(e,undefined,undefined); + if(avIndex !== undefined) { + if(a !== undefined) { + let level = avIndex.index[a]; + if(level) { + this.removeVs(level.index, changes, scope, e, level.value); + } + } else { + let keys = Object.keys(avIndex.index); + for(let key of keys) { + let level = avIndex.index[key]; + this.removeVs(level.index, changes, scope, e, level.value); + } + } + } + } + } +} + +export class SetAction extends Action { + execute(multiIndex, row, changes) { + let [e,a,v] = this.resolve(row); + // multidb + for(let scope of this.scopes) { + let curIndex = multiIndex.getIndex(scope); + let vIndex = curIndex.lookup(e,a,undefined); + if(vIndex !== undefined) { + let keys = Object.keys(vIndex.index); + for(let key of keys) { + let value = vIndex.index[key].value; + if(value !== v) { + changes.unstore(scope,e,a,value); + } + } + } + changes.store(scope,e,a,v,this.node); + } + } +} + +export var ActionImplementations = { + ":=": SetAction, + "+=": InsertAction, + "-=": RemoveAction, + "erase": EraseAction, +} + +export function executeActions(multiIndex: MultiIndex, actions: Action[], rows: any[], changes: Changes, capture = false) { + if(capture) { + changes.capture(); + } + for(let row of rows) { + for(let action of actions) { + action.execute(multiIndex, row, changes); + } + } + if(capture) { + return changes.captureEnd(); + } +} diff --git a/src/runtime/analyzer.ts b/src/runtime/analyzer.ts new file mode 100644 index 000000000..b06ea21dd --- /dev/null +++ b/src/runtime/analyzer.ts @@ -0,0 +1,877 @@ +//--------------------------------------------------------------------- +// Analyzer +//--------------------------------------------------------------------- + +import {ParseBlock, ParseNode, nodeToBoundaries} from "./parser" +import {Evaluation, Database} from "./runtime" +import {TripleIndex} from "./indexes" +import {Changes} from "./changes" +import * as join from "./join" +import * as parser from "./parser" +import * as builder from "./builder" +import * as eveSource from "./eveSource"; +import {BrowserSessionDatabase} from "./databases/browserSession" + +enum ActionType { Bind, Commit } + +//--------------------------------------------------------------------- +// AnalysisContext +//--------------------------------------------------------------------- + +class AnalysisContext { + ScanId = 0; + changes: Changes + block: ParseBlock + spans: any[]; + extraInfo: any; + + constructor(spans, extraInfo) { + this.spans = spans; + this.extraInfo = extraInfo; + } + + record(parseNode: any, kind: "action" | "scan") { + let changes = this.changes; + let recordId = parseNode.id; + let [start, stop] = nodeToBoundaries(parseNode); + changes.store("session", recordId, "tag", "record", "analyzer"); + changes.store("session", recordId, "block", this.block.id, "analyzer"); + changes.store("session", recordId, "start", start, "analyzer"); + changes.store("session", recordId, "stop", stop, "analyzer"); + changes.store("session", recordId, "entity", parseNode.variable.id, "analyzer"); + changes.store("session", recordId, "kind", kind, "analyzer"); + for(let scope of parseNode.scopes) { + changes.store("session", recordId, "scopes", scope, "analyzer"); + } + return recordId; + } + + scan(parseNode: any, scopes: string[], entity: any, attribute: string, value: any) { + let changes = this.changes; + let scanId = parseNode.id; + let [start, stop] = nodeToBoundaries(parseNode, this.block.start); + changes.store("session", scanId, "tag", "scan", "analyzer"); + changes.store("session", scanId, "block", this.block.id, "analyzer"); + changes.store("session", scanId, "start", start, "analyzer"); + changes.store("session", scanId, "stop", stop, "analyzer"); + changes.store("session", scanId, "entity", entity.id, "analyzer"); + if(attribute !== undefined) { + changes.store("session", scanId, "attribute", attribute, "analyzer"); + } + if(parseNode.buildId !== undefined) { + changes.store("session", scanId, "build-node", parseNode.buildId, "analyzer"); + } + if(value && value.id !== undefined) { + changes.store("session", scanId, "value", value.id, "analyzer"); + changes.store("session", value.id, "tag", "variable", "analyzer"); + } else if(value !== undefined) { + changes.store("session", scanId, "value", value, "analyzer"); + } + for(let scope of scopes) { + changes.store("session", scanId, "scopes", scope, "analyzer"); + } + return scanId; + } + + provide(parseNode: any, scopes: string[], entity: any, attribute: string, value: any) { + let changes = this.changes; + let actionId = parseNode.id; + let [start, stop] = nodeToBoundaries(parseNode, this.block.start); + changes.store("session", actionId, "tag", "action", "analyzer"); + changes.store("session", actionId, "block", this.block.id, "analyzer"); + changes.store("session", actionId, "start", start, "analyzer"); + changes.store("session", actionId, "stop", stop, "analyzer"); + changes.store("session", actionId, "entity", entity.id, "analyzer"); + changes.store("session", actionId, "attribute", attribute, "analyzer"); + if(parseNode.buildId !== undefined) { + changes.store("session", actionId, "build-node", parseNode.buildId, "analyzer"); + } + if(value.id !== undefined) { + changes.store("session", actionId, "value", value.id, "analyzer"); + changes.store("session", value.id, "tag", "variable", "analyzer"); + } else { + changes.store("session", actionId, "value", value, "analyzer"); + } + for(let scope of scopes) { + changes.store("session", actionId, "scopes", scope, "analyzer"); + } + return actionId; + } + + value(node) { + if(node.type === "constant") return node.value; + if(node.type === "variable") return node; + throw new Error("Trying to get value of non-value type: " + node.type) + } +} + +//--------------------------------------------------------------------- +// Analysis +//--------------------------------------------------------------------- + +class Analysis { + + changes: Changes + + constructor(changes) { + this.changes = changes; + } + + //--------------------------------------------------------------------- + // Scans + //--------------------------------------------------------------------- + + _scans(context: AnalysisContext, scans) { + for(let scan of scans) { + if(scan.type === "record") { + this._scanRecord(context, scan); + } else if(scan.type === "scan") { + this._scanScan(context, scan); + } else if(scan.type === "ifExpression") { + this._scanIf(context, scan); + } else if(scan.type === "not") { + this._scanNot(context, scan); + } + } + } + + _scanRecord(context: AnalysisContext, node) { + context.record(node, "scan"); + for(let attr of node.attributes) { + if(attr.value.type === "parenthesis") { + for(let item of attr.value.items) { + let id = context.scan(item, node.scopes, node.variable, attr.attribute, context.value(item)); + } + } else { + let id = context.scan(attr, node.scopes, node.variable, attr.attribute, context.value(attr.value)); + } + } + } + + _scanScan(context: AnalysisContext, node) { + if(node.attribute === undefined || node.attribute.type === "variable") { + let value; + if(node.value !== undefined) { + value = context.value(node.value); + } + let id = context.scan(node, node.scopes, context.value(node.entity), undefined, value); + } else { + let id = context.scan(node, node.scopes, context.value(node.entity), context.value(node.attribute), context.value(node.value)); + } + } + + _scanIf(context: AnalysisContext, ifExpression) { + + } + _scanNot(context: AnalysisContext, not) { + + } + + //--------------------------------------------------------------------- + // Expressions + //--------------------------------------------------------------------- + + _expressions(context: AnalysisContext, expressions) { + for(let expression of expressions) { + if(expression.type === "expression") { + + } else if(expression.type === "functionRecord") { + + } + } + + } + + //--------------------------------------------------------------------- + // Actions + //--------------------------------------------------------------------- + + _actions(context: AnalysisContext, type: ActionType, actions) { + for(let action of actions) { + if(action.type === "record") { + this._actionRecord(context, action); + } else if(action.type === "action") { + this._actionAction(context, action); + } + } + } + + _actionRecord(context: AnalysisContext, node) { + context.record(node, "action"); + for(let attr of node.attributes) { + if(attr.value.type === "parenthesis") { + for(let item of attr.value.items) { + let id = context.provide(item, node.scopes, node.variable, attr.attribute, context.value(item)); + } + } else { + let id = context.provide(attr, node.scopes, node.variable, attr.attribute, context.value(attr.value)); + } + } + } + + _actionAction(context: AnalysisContext, node) { + if(node.action === "erase") { + // if(node.attribute === undefined) { + // context.provide(node.scopes, "any", ""); + // } else { + // context.provide(node.scopes, "all", ""); + // } + } else { + let attribute = typeof node.attribute === "string" ? node.attribute : context.value(node.attribute); + if(node.value.type === "parenthesis") { + for(let item of node.value.items) { + let id = context.provide(item, node.scopes, node.entity, attribute, context.value(item)); + } + } else { + let id = context.provide(node, node.scopes, node.entity, attribute, context.value(node.value)); + } + } + } + + //--------------------------------------------------------------------- + // Variables + //--------------------------------------------------------------------- + + _variables(context: AnalysisContext, variables) { + let changes = context.changes; + for(let name of Object.keys(variables)) { + let variable = variables[name]; + changes.store("session", variable.id, "tag", "variable"); + changes.store("session", variable.id, "name", variable.name); + changes.store("session", variable.id, "block", context.block.id); + if(variable.register !== undefined) { + changes.store("session", variable.id, "register", variable.register); + } + if(variable.generated) { + changes.store("session", variable.id, "tag", "generated"); + } + if(variable.nonProjecting) { + changes.store("session", variable.id, "tag", "non-projecting"); + } + } + } + + //--------------------------------------------------------------------- + // Equalities + //--------------------------------------------------------------------- + + _equalities(context: AnalysisContext, equalities) { + let changes = context.changes; + let ix = 0; + for(let [a, b] of equalities) { + let equalityId = `${context.block.id}|equality|${ix++}`; + a = context.value(a); + b = context.value(b); + let aId = a.id ? a.id : a; + let bId = b.id ? b.id : b; + changes.store("session", equalityId, "tag", "equality"); + changes.store("session", equalityId, "block", context.block.id); + changes.store("session", equalityId, "a", aId); + changes.store("session", equalityId, "b", bId); + } + } + + //--------------------------------------------------------------------- + // Links + //--------------------------------------------------------------------- + + _link(context, aId, bId) { + let changes = context.changes; + if(!aId || !bId) throw new Error("WAT"); + let linkId = `${context.block.id}|link|${context.ScanId++}`; + changes.store("session", linkId, "tag", "link"); + changes.store("session", linkId, "block", context.block.id); + changes.store("session", linkId, "a", aId); + changes.store("session", linkId, "b", bId); + } + + _links(context: AnalysisContext, links) { + for(let ix = 0, len = links.length; ix < len; ix += 2) { + let aId = links[ix]; + let bId = links[ix + 1]; + this._link(context, aId, bId); + } + } + + //--------------------------------------------------------------------- + // Tokens + //--------------------------------------------------------------------- + + _tokens(context, tokens) { + let changes = context.changes; + for(let token of tokens) { + let tokenId = token.id; + changes.store("session", tokenId, "tag", "token"); + changes.store("session", tokenId, "block", context.block.id); + changes.store("session", tokenId, "start", token.startOffset); + changes.store("session", tokenId, "stop", token.endOffset); + } + } + + //--------------------------------------------------------------------- + // Block + //--------------------------------------------------------------------- + + _block(context: AnalysisContext, block: ParseBlock) { + context.changes.store("session", block.id, "tag", "block"); + this._links(context, block.links); + this._tokens(context, block.tokens); + this._variables(context, block.variables); + this._equalities(context, block.equalities); + this._scans(context, block.scanLike); + this._expressions(context, block.expressions); + this._actions(context, ActionType.Bind, block.binds); + this._actions(context, ActionType.Commit, block.commits); + } + + //--------------------------------------------------------------------- + // Public + //--------------------------------------------------------------------- + + block(block: ParseBlock, spans, extraInfo) { + let context = this.createContext(block, spans, extraInfo); + this._block(context, block); + } + + createContext(block: ParseBlock, spans, extraInfo) { + let context = new AnalysisContext(spans, extraInfo); + context.block = block; + context.changes = this.changes; + return context; + } + +} + +export class EditorDatabase extends Database { + spans: any[]; + extraInfo: any; + + constructor(spans, extraInfo) { + super(); + this.spans = spans; + this.extraInfo = extraInfo; + } + + onFixpoint(evaluation: Evaluation, changes: Changes) { + super.onFixpoint(evaluation, changes); + let name = evaluation.databaseToName(this); + let index = this.index; + let comments = index.alookup("tag", "comment"); + if(comments) { + for(let commentId of Object.keys(comments.index)) { + let comment = index.asObject(commentId, false, true); + this.spans.push(comment.start, comment.stop, "document_comment", commentId); + comment.spanId = commentId; + this.extraInfo[commentId] = comment; + } + } + } +} + +function makeEveAnalyzer() { + if(eve) return eve; + let {results, errors} = parser.parseDoc(eveSource.get("analyzer.eve"), "analyzer"); + let {text, spans, extraInfo} = results; + let {blocks, errors: buildErrors} = builder.buildDoc(results); + if(errors.length || buildErrors.length) { + console.error("ANALYZER CREATION ERRORS", errors, buildErrors); + } + // let browserDb = new BrowserSessionDatabase(browser.responder); + let session = new Database(); + session.blocks = blocks; + let evaluation = new Evaluation(); + evaluation.registerDatabase("session", session); + // evaluation.registerDatabase("browser", browserDb); + return evaluation; +} + +let eve; + +export function analyze(blocks: ParseBlock[], spans: any[], extraInfo: any) { + console.time("load analysis"); + eve = makeEveAnalyzer(); + let session = new Database(); + let prev = eve.getDatabase("session") + session.blocks = prev.blocks; + // console.log("ANALYZER BLOCKS", session.blocks); + eve.unregisterDatabase("session"); + eve.registerDatabase("session", session); + let editorDb = new EditorDatabase(spans, extraInfo); + eve.unregisterDatabase("editor"); + eve.registerDatabase("editor", editorDb); + eve.fixpoint(); + let changes = eve.createChanges(); + let analysis = new Analysis(changes); + for(let block of blocks) { + analysis.block(block, spans, extraInfo); + } + changes.commit(); + // console.log(changes); + console.timeEnd("load analysis"); + // eve.executeActions([], changes); +} + + +let prevQuery; +function doQuery(queryId, query, spans, extraInfo) { + eve = makeEveAnalyzer(); + let editorDb = new EditorDatabase(spans, extraInfo); + eve.unregisterDatabase("editor"); + eve.registerDatabase("editor", editorDb); + let changes = eve.createChanges(); + if(prevQuery) { + changes.unstoreObject(prevQuery.queryId, prevQuery.query, "analyzer", "session"); + } + changes.storeObject(queryId, query, "analyzer", "session"); + eve.executeActions([], changes); + prevQuery = {queryId, query}; + return eve; +} + +export function tokenInfo(evaluation: Evaluation, tokenId: string, spans: any[], extraInfo: any) { + let queryId = `query|${tokenId}`; + let query = {tag: "query", token: tokenId}; + let eve = doQuery(queryId, query, spans, extraInfo); + + // look at the results and find out which action node we were looking + // at + let sessionIndex = eve.getDatabase("session").index; + let queryInfo = sessionIndex.alookup("tag", "query"); + let evSession = evaluation.getDatabase("session"); + if(queryInfo) { + for(let entity of Object.keys(queryInfo.index)) { + let info = sessionIndex.asObject(entity); + + console.log("INFO", info); + // why is this failing? + let nodeArray = info.scan || info.action; + if(nodeArray) { + let node = sessionIndex.asObject(nodeArray[0]); + let blockId = node["block"][0]; + let found; + for(let block of evSession.blocks) { + console.log("BLOCK ID", block.id, node["block"]); + if(block.id === blockId) { + found = block; + break; + } + } + console.log("NODE BLOCK", blockId, found); + console.log("FAILING SCAN", blockToFailingScan(found)); + console.log("CARDINALITIES", resultsToCardinalities(found.results)) + console.log("SPECIFIC ROWS", findResultRows(found.results, 2, "cherry")) + } + + // look for the facts that action creates + if(info.action) { + for(let actionId of info.action) { + let action = sessionIndex.asObject(actionId); + let evIndex = evaluation.getDatabase(action.scopes[0]).index; + let nodeItems = evIndex.nodeLookup(action["build-node"][0]); + if(nodeItems) { + console.log("ACTION", action["build-node"][0]); + console.log(evIndex.toTriples(false, nodeItems.index)); + } + } + } + } + } +} + +export function findCardinality(evaluation: Evaluation, info: any, spans: any[], extraInfo: any) { + let queryId = `query|${info.requestId}`; + let query = {tag: ["query", "findCardinality"], token: info.variable}; + let eve = doQuery(queryId, query, spans, extraInfo); + + let sessionIndex = eve.getDatabase("session").index; + let evSession = evaluation.getDatabase("session"); + let lookup = {}; + let blockId; + let cardinalities; + + let queryInfo = sessionIndex.alookup("tag", "query"); + if(queryInfo) { + let [entity] = queryInfo.toValues(); + let obj = sessionIndex.asObject(entity); + if(obj.register) { + for(let variable of obj.register) { + let varObj = sessionIndex.asObject(variable); + if(varObj) { + if(!blockId) { + let found; + blockId = varObj.block[0]; + for(let block of evSession.blocks) { + if(block.id === blockId) { + found = block; + break; + } + } + cardinalities = resultsToCardinalities(found.results); + } + lookup[varObj.token[0]] = cardinalities[varObj.register[0]].cardinality; + } + } + } + } + info.cardinality = lookup; + return info; +} + +export function findValue(evaluation: Evaluation, info: any, spans: any[], extraInfo: any) { + let queryId = `query|${info.requestId}`; + let query = {tag: ["query", "findValue"], token: info.variable}; + let eve = doQuery(queryId, query, spans, extraInfo); + + let sessionIndex = eve.getDatabase("session").index; + let evSession = evaluation.getDatabase("session"); + let lookup = {}; + let blockId, found; + let rows = []; + let varToRegister = {}; + let names = {}; + + let queryInfo = sessionIndex.alookup("tag", "query"); + if(queryInfo) { + let [entity] = queryInfo.toValues(); + let obj = sessionIndex.asObject(entity); + if(obj.register) { + for(let variable of obj.register) { + let varObj = sessionIndex.asObject(variable); + if(varObj) { + if(!blockId) { + blockId = varObj.block[0]; + for(let block of evSession.blocks) { + if(block.id === blockId) { + found = block; + break; + } + } + } + if(varObj.attribute) { + for(let attribute of varObj.attribute) { + varToRegister[attribute] = varObj.register[0]; + } + } + lookup[varObj.token[0]] = varObj.register[0]; + names[varObj.token[0]] = varObj.name[0]; + } + } + } + } + if(info.given) { + let keys = Object.keys(info.given); + let registers = []; + let registerValues = []; + for(let key of keys) { + let reg = varToRegister[key]; + if(reg !== undefined && registers.indexOf(reg) === -1) { + registers.push(reg); + registerValues.push(info.given[key][0]); + } + } + rows = findResultRows(found.results, registers, registerValues); + } else { + rows = found.results; + } + info.rows = rows.slice(0,100); + info.totalRows = rows.length; + info.variableMappings = lookup; + info.variableNames = names; + return info; +} + + +export function nodeIdToRecord(evaluation, nodeId, spans, extraInfo) { + let queryId = `query|${nodeId}`; + let query = {tag: "query", "build-node": nodeId}; + let eve = doQuery(queryId, query, spans, extraInfo); + + let sessionIndex = eve.getDatabase("session").index; + let queryInfo = sessionIndex.alookup("tag", "query"); + if(queryInfo) { + let [entity] = queryInfo.toValues(); + let obj = sessionIndex.asObject(entity); + if(obj.pattern) { + return obj.pattern[0] + } + } + return; +} + +export function findRecordsFromToken(evaluation, info, spans, extraInfo) { + let queryId = `query|${info.requestId}`; + let query: any = {tag: ["findRecordsFromToken"]}; + if(info.token) query.token = info.token; + + let evSession = evaluation.getDatabase("session"); + let evBrowser = evaluation.getDatabase("browser"); + evSession.nonExecuting = true; + evBrowser.nonExecuting = true; + eve.registerDatabase("evaluation-session", evSession); + eve.registerDatabase("evaluation-browser", evBrowser); + doQuery(queryId, query, spans, extraInfo); + eve.unregisterDatabase("evaluation-session"); + eve.unregisterDatabase("evaluation-browser"); + evSession.nonExecuting = false; + evBrowser.nonExecuting = false; + + let sessionIndex = eve.getDatabase("session").index; + let queryInfo = sessionIndex.alookup("tag", "findRecordsFromToken"); + if(queryInfo) { + let [entity] = queryInfo.toValues(); + let obj = sessionIndex.asObject(entity); + console.log("FIND RECORDS", obj); + if(obj.record) { + return info.record = obj.record; + } else { + info.record = []; + return info; + } + } + return; +} + + +export function findSource(evaluation, info, spans, extraInfo) { + let queryId = `query|${info.requestId}`; + let query: any = {tag: ["query", "findSource"]}; + if(info.record) query.recordId = info.record; + if(info.attribute) query.attribute = info.attribute; + if(info.span) query.span = info.span; + + let evSession = evaluation.getDatabase("session"); + let evBrowser = evaluation.getDatabase("browser"); + evSession.nonExecuting = true; + evBrowser.nonExecuting = true; + eve.registerDatabase("evaluation-session", evSession); + eve.registerDatabase("evaluation-browser", evBrowser); + doQuery(queryId, query, spans, extraInfo); + eve.unregisterDatabase("evaluation-session"); + eve.unregisterDatabase("evaluation-browser"); + evSession.nonExecuting = false; + evBrowser.nonExecuting = false; + + let sessionIndex = eve.getDatabase("session").index; + let queryInfo = sessionIndex.alookup("tag", "findSource"); + if(queryInfo) { + let [entity] = queryInfo.toValues(); + let obj = sessionIndex.asObject(entity); + console.log("FIND SOURCE", obj); + if(obj.source) { + info.source = obj.source.map((source) => sessionIndex.asObject(source, false, true)); + return info; + } else if(obj.block) { + info.block = obj.block; + return info; + } else { + info.block = []; + info.source = []; + return info; + } + } + return; +} + +export function findRelated(evaluation, info, spans, extraInfo) { + let queryId = `query|${info.requestId}`; + let query: any = {tag: ["query", "findRelated"]}; + let queryType; + if(info.span) { + query.span = info.span; + queryType = "span"; + } + if(info.variable) { + query.variable = info.variable; + queryType = "variable" + } + query.for = queryType + + let evSession = evaluation.getDatabase("session"); + eve.registerDatabase("evaluation-session", evSession); + doQuery(queryId, query, spans, extraInfo); + eve.unregisterDatabase("evaluation-session"); + + let sessionIndex = eve.getDatabase("session").index; + let queryInfo = sessionIndex.alookup("tag", "findRelated"); + if(queryInfo) { + let [entity] = queryInfo.toValues(); + let obj = sessionIndex.asObject(entity); + if(queryType === "span" && obj.variable) { + info.variable = obj.variable; + } else if(queryType === "variable" && obj.span) { + info.span = obj.span; + } else { + info.variable = []; + info.span = []; + } + return info; + } + return; +} + +export function findAffector(evaluation, info, spans, extraInfo) { + let queryId = `query|${info.requestId}`; + let query: any = {tag: ["query", "findAffector"]}; + if(info.record) query.recordId = info.record; + if(info.attribute) query.attribute = info.attribute; + if(info.span) query.span = info.span; + + let evSession = evaluation.getDatabase("session"); + let evBrowser = evaluation.getDatabase("browser"); + evSession.nonExecuting = true; + evBrowser.nonExecuting = true; + eve.registerDatabase("evaluation-session", evSession); + eve.registerDatabase("evaluation-browser", evBrowser); + doQuery(queryId, query, spans, extraInfo); + eve.unregisterDatabase("evaluation-session"); + eve.unregisterDatabase("evaluation-browser"); + evSession.nonExecuting = false; + evBrowser.nonExecuting = false; + + let sessionIndex = eve.getDatabase("session").index; + let queryInfo = sessionIndex.alookup("tag", "findAffector"); + if(queryInfo) { + let [entity] = queryInfo.toValues(); + let obj = sessionIndex.asObject(entity); + console.log("FIND AFFECTOR", obj); + if(obj.affector) { + info.affector = obj.affector.map((affector) => sessionIndex.asObject(affector, false, true)); + return info; + } else { + info.affector = []; + return info; + } + } + return; +} + +export function findFailure(evaluation, info, spans, extraInfo) { + let evSession = evaluation.getDatabase("session"); + let failingSpans = info.span = []; + let sessionIndex = eve.getDatabase("session").index; + + for(let queryBlockId of info.block) { + let found; + for(let block of evSession.blocks) { + if(block.id === queryBlockId) { + found = block; + break; + } + } + let scan = blockToFailingScan(found); + if(scan) { + let level = sessionIndex.alookup("build-node", scan.id); + if(level) { + let analyzerScanId = level.toValues()[0]; + let analyzerScan = sessionIndex.asObject(analyzerScanId, false, true); + + failingSpans.push({id: analyzerScanId, buildId: scan.id, block: found.id, start: analyzerScan.start, stop: analyzerScan.stop}); + } + } + } + return info; +} + +export function findRootDrawers(evaluation, info, spans, extraInfo) { + let queryId = `query|${info.requestId}`; + let query = {tag: "findRootDrawers"}; + let eve = doQuery(queryId, query, spans, extraInfo); + + let sessionIndex = eve.getDatabase("session").index; + let queryInfo = sessionIndex.alookup("tag", "findRootDrawers"); + if(queryInfo) { + let [entity] = queryInfo.toValues(); + let obj = sessionIndex.asObject(entity); + if(obj.drawer) { + info.drawers = obj.drawer.map((id) => sessionIndex.asObject(id, false, true)); + } else { + info.drawers = []; + } + } + return info; +} + +export function findMaybeDrawers(evaluation, info, spans, extraInfo) { + let queryId = `query|${info.requestId}`; + let query = {tag: "findMaybeDrawers"}; + let eve = doQuery(queryId, query, spans, extraInfo); + + let sessionIndex = eve.getDatabase("session").index; + let queryInfo = sessionIndex.alookup("tag", "findMaybeDrawers"); + if(queryInfo) { + let [entity] = queryInfo.toValues(); + let obj = sessionIndex.asObject(entity); + if(obj.drawer) { + info.drawers = obj.drawer.map((id) => sessionIndex.asObject(id, false, true)); + } else { + info.drawers = []; + } + } + return info; +} + + + +function blockToFailingScan(block) { + let scan; + for(let stratum of block.strata) { + if(stratum.resultCount === 0) { + let {solverInfo} = stratum; + let scanIx = 0; + let maxFailures = 0; + let maxIx = 0; + for(let failures of solverInfo) { + if(failures > maxFailures) { + maxFailures = failures; + maxIx = scanIx; + } + scanIx++; + } + scan = stratum.scans[maxIx]; + break; + } + } + return scan; +} + +function resultsToCardinalities(results) { + let cardinalities = []; + let ix = 0; + while(ix < results[0].length) { + cardinalities[ix] = {cardinality: 0, values: {}}; + ix++; + } + + for(let result of results) { + let ix = 0; + for(let value of result) { + let info = cardinalities[ix]; + if(!info.values[value]) { + info.values[value] = true; + info.cardinality++; + } + ix++; + } + } + + return cardinalities; +} + +function findResultRows(results, registers, values) { + let found = []; + for(let result of results) { + let skip; + let ix = 0; + for(let register of registers) { + if(result[register] !== values[ix]) { + skip = true; + break; + } + ix++; + } + if(!skip) { + found.push(result); + } + } + return found; +} diff --git a/src/runtime/block.ts b/src/runtime/block.ts new file mode 100644 index 000000000..d90de1b7c --- /dev/null +++ b/src/runtime/block.ts @@ -0,0 +1,290 @@ +//--------------------------------------------------------------------- +// Block +//--------------------------------------------------------------------- + +import {Variable, isVariable, Scan, NotScan, IfScan, ProposalProvider, JoinOptions, join} from "./join"; +import {MultiIndex} from "./indexes"; +import {Changes, ChangesIndex, ChangeType} from "./changes"; +import {Action, executeActions} from "./actions"; +import {Aggregate} from "./providers/aggregate" +import {PerformanceTracker} from "./performance"; + +//--------------------------------------------------------------------- +// DependencyChecker +//--------------------------------------------------------------------- + +export class DependencyChecker { + dependencies: any; + alwaysTrue: boolean; + + constructor(block) { + this.alwaysTrue = block.singleRun; + let map = this.buildVariableMap(block); + this.dependencies = this.buildDependencies(map); + } + + buildVariableMap(block, variableMap = {"any": {attributes: {}}}) { + for(let level of block.strata) { + for(let scan of level.scans) { + if(scan instanceof Scan) { + let {e,a,v} = scan; + let cur; + if(isVariable(e)) { + cur = variableMap[e.id]; + if(cur === undefined) { + cur = variableMap[e.id] = {attributes: {}}; + } + } else { + cur = variableMap["any"]; + } + if(!isVariable(a)) { + let attrInfo = cur.attributes[a]; + if(attrInfo === undefined) { + attrInfo = cur.attributes[a] = {values: []}; + } + if(!isVariable(v)) { + cur.attributes[a].values.push(v); + } else { + attrInfo.any = true; + } + } else { + cur.any = true; + } + } else if(scan instanceof NotScan) { + // this.alwaysTrue = true; + this.buildVariableMap(scan, variableMap); + } else if(scan instanceof IfScan) { + // this.alwaysTrue = true; + for(let branch of scan.branches) { + this.buildVariableMap(branch, variableMap); + } + } + } + } + return variableMap; + } + + _depsForTag(deps, attributes, tag) { + let attributeIndex = deps[tag]; + if(!attributeIndex) { + attributeIndex = deps[tag] = {}; + } + for(let attribute of Object.keys(attributes)) { + let attributeInfo = attributes[attribute]; + let vIndex = attributeIndex[attribute]; + if(!vIndex && !attributeInfo.any) { + vIndex = attributeIndex[attribute] = {}; + } else if(attributeInfo.any || vIndex === true) { + attributeIndex[attribute] = true; + continue; + } + for(let value of attributeInfo.values) { + vIndex[value] = true; + } + } + } + + buildDependencies(variableMap) { + let deps = {"any": {"tag": {}}}; + for(let variableId of Object.keys(variableMap)) { + let {any, attributes} = variableMap[variableId]; + if(any) { + this.alwaysTrue = true; + } + let tagAttributes = attributes["tag"]; + if(!tagAttributes || tagAttributes.any) { + this._depsForTag(deps, attributes, "any") + } else { + for(let tag of tagAttributes.values) { + if(deps["any"]["tag"] === true) break; + deps["any"]["tag"][tag] = true; + this._depsForTag(deps, attributes, tag); + } + } + } + return deps; + } + + check(multiIndex: MultiIndex, change, tags, e, a, v) { + //multidb + if(this.alwaysTrue) return true; + let deps = this.dependencies; + if(tags.length === 0) { + let attrIndex = deps["any"]; + if(!attrIndex) return false; + let attr = attrIndex[a]; + if(attr === true) return true; + if(attr === undefined) return false; + return attr[v]; + } + if(deps["any"]) { + let attr = deps["any"][a]; + if(attr === true) return true; + if(attr === true && attr[v] === true) return true + } + for(let tag of tags) { + let attrIndex = deps[tag]; + if(!attrIndex) continue; + let attr = attrIndex[a]; + if(attr === undefined) continue; + if(attr === true || attr[v] === true) return true; + } + return false + } +} + +//--------------------------------------------------------------------- +// Block +//--------------------------------------------------------------------- + +function hasDatabaseScan(strata) { + for(let stratum of strata) { + for(let scan of stratum.scans) { + if(scan instanceof Scan) return true; + if(scan instanceof IfScan) return true; + if(scan instanceof NotScan) return true; + } + } + return false; +} + +export function scansToVars(scans, output = []) { + for(let scan of scans) { + for(let variable of scan.vars) { + if(variable) { + output[variable.id] = variable; + } + } + } + return output; +} + +export class BlockStratum { + solverInfo = []; + resultCount = 0; + results: any[]; + scans: ProposalProvider[]; + aggregates: Aggregate[]; + vars: Variable[]; + constructor(scans, aggregates = []) { + this.scans = scans; + this.aggregates = aggregates; + let vars = []; + scansToVars(scans, vars); + this.vars = vars; + } + + execute(multiIndex: MultiIndex, rows: any[], options: JoinOptions = {}) { + let ix = 0; + for(let scan of this.scans) { + this.solverInfo[ix] = 0; + ix++; + } + let results = []; + for(let aggregate of this.aggregates) { + aggregate.aggregate(rows); + } + for(let row of rows) { + options.rows = results; + options.solverInfo = this.solverInfo; + results = join(multiIndex, this.scans, this.vars, row, options); + } + this.resultCount = results.length; + this.results = results; + return results; + } +} + +export class Block { + static BlockId = 0; + id: any; + strata: BlockStratum[]; + commitActions: Action[]; + bindActions: Action[]; + name: string; + vars: Variable[]; + solvingVars: Variable[]; + dormant: boolean; + singleRun: boolean; + prevInserts: ChangesIndex; + checker: DependencyChecker; + parse: any; + results: any[]; + + constructor(name: string, strata: BlockStratum[], commitActions: Action[], bindActions: Action[], parse?: any) { + this.id = parse.id || Block.BlockId++; + this.name = name; + this.strata = strata; + this.commitActions = commitActions; + this.bindActions = bindActions; + this.parse = parse; + + this.dormant = false; + if(!hasDatabaseScan(strata)) { + this.singleRun = true; + } + + let blockVars = []; + scansToVars(strata, blockVars); + scansToVars(commitActions, blockVars); + scansToVars(bindActions, blockVars); + + this.vars = blockVars; + this.prevInserts = new ChangesIndex(); + this.checker = new DependencyChecker(this); + } + + updateBinds(diff, changes) { + let newPositions = diff.positions; + let newInfo = diff.info; + let {positions, info} = this.prevInserts; + for(let key of Object.keys(positions)) { + let pos = positions[key]; + let type = info[pos]; + let neuePos = newPositions[key]; + let neueType = newInfo[neuePos]; + // if this was added + if(neueType === undefined) { + let e = info[pos + 1]; + let a = info[pos + 2]; + let v = info[pos + 3]; + let node = info[pos + 4]; + let scope = info[pos + 5]; + changes.unstore(scope,e,a,v,node); + } + } + } + + execute(multiIndex: MultiIndex, changes: Changes) { + if(this.dormant) { + return changes; + } else if(this.singleRun) { + this.dormant = true; + } + // console.groupCollapsed(this.name); + // console.log("--- " + this.name + " --------------------------------"); + let results = [[]]; + for(let stratum of this.strata) { + results = stratum.execute(multiIndex, results); + if(results.length === 0) break; + } + this.results = results; + // console.log("results :: ", time(start)); + // console.log(" >>> RESULTS") + // console.log(results); + // console.log(" <<<< RESULTS") + if(this.commitActions.length !== 0) { + executeActions(multiIndex, this.commitActions, results, changes); + } + + if(this.bindActions.length !== 0) { + let diff = executeActions(multiIndex, this.bindActions, results, changes, true); + this.updateBinds(diff, changes); + this.prevInserts = diff; + } + + // console.log(changes); + // console.groupEnd(); + return changes; + } +} diff --git a/src/runtime/browser.ts b/src/runtime/browser.ts new file mode 100644 index 000000000..95eca8787 --- /dev/null +++ b/src/runtime/browser.ts @@ -0,0 +1,78 @@ +//--------------------------------------------------------------------- +// Browser +//--------------------------------------------------------------------- + +import {Evaluation, Database} from "./runtime"; +import * as join from "./join"; +import {EveClient, client} from "../client"; +import * as parser from "./parser"; +import * as builder from "./builder"; +import {ids} from "./id"; +import {RuntimeClient} from "./runtimeClient"; +import {HttpDatabase} from "./databases/http"; +import {BrowserViewDatabase, BrowserEditorDatabase, BrowserInspectorDatabase} from "./databases/browserSession"; + +//--------------------------------------------------------------------- +// Utils +//--------------------------------------------------------------------- + +// this makes me immensely sad... +function download(filename, text) { + var element = document.createElement('a'); + element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); + element.setAttribute('download', filename); + element.style.display = 'none'; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); +} + +//--------------------------------------------------------------------- +// Responder +//--------------------------------------------------------------------- + +class BrowserRuntimeClient extends RuntimeClient { + client: EveClient; + + constructor(client:EveClient) { + let dbs = { + "http": new HttpDatabase() + } + if(client.showIDE) { + dbs["view"] = new BrowserViewDatabase(); + dbs["editor"] = new BrowserEditorDatabase(); + dbs["inspector"] = new BrowserInspectorDatabase(); + } + super(dbs); + this.client = client; + } + + send(json) { + setTimeout(() => { + this.client.onMessage({data: json}); + }, 0); + } + +} + +export var responder: BrowserRuntimeClient; + +//--------------------------------------------------------------------- +// Init a program +//--------------------------------------------------------------------- + +export function init(code) { + global["browser"] = true; + + responder = new BrowserRuntimeClient(client); + responder.load(code || "", "user"); + + global["evaluation"] = responder; + + global["save"] = () => { + responder.handleEvent(JSON.stringify({type: "dumpState"})); + } + + // client.socket.onopen(); + // responder.handleEvent(JSON.stringify({type: "findPerformance", requestId: 2})); +} diff --git a/src/runtime/builder.ts b/src/runtime/builder.ts new file mode 100644 index 000000000..66ab5e551 --- /dev/null +++ b/src/runtime/builder.ts @@ -0,0 +1,765 @@ +//----------------------------------------------------------- +// Builder +//----------------------------------------------------------- + +import * as join from "./join"; +import * as providers from "./providers/index"; +import "./providers/math"; +import "./providers/logical"; +import "./providers/string"; +import * as errors from "./errors"; +import {Sort} from "./providers/sort"; +import {Aggregate} from "./providers/aggregate"; +import {ActionImplementations} from "./actions"; +import {Block, BlockStratum} from "./block"; + +//----------------------------------------------------------- +// Runtime helpers +//----------------------------------------------------------- + +function clone(map) { + let neue = {}; + for(let key of Object.keys(map)) { + neue[key] = map[key]; + } + return neue; +} + +//----------------------------------------------------------- +// Builder Context +//----------------------------------------------------------- + +class BuilderContext { + errors: any[] = []; + groupIx: number; + varIx: number; + variableToGroup: any; + groupToValue: any; + unprovided: boolean[]; + registerToVars: any; + myRegisters: boolean[]; + nonProviding: boolean; + + constructor(block, variableToGroup = {}, groupToValue = {}, unprovided = [], registerToVars = {}, groupIx = 0, varIx = 0) { + this.variableToGroup = variableToGroup; + this.groupToValue = groupToValue; + this.unprovided = unprovided; + this.groupIx = groupIx; + this.varIx = varIx; + this.registerToVars = registerToVars; + this.myRegisters = []; + this.assignGroups(block); + this.assignRuntimeVariables(block); + this.nonProviding = false; + } + + getValue(node) { + if(node.type === "variable") { + let group = this.variableToGroup[node.name]; + if(group === undefined) { + throw new Error("Variable with no group: " + node); + } + let value = this.groupToValue[group]; + if(value === undefined) throw new Error("Group with no value" + node); + return value; + } else if(node.type === "parenthesis") { + let values = []; + for(let item of node.items) { + values.push(this.getValue(item)); + } + return values; + } else if(node.type === "constant") { + return node.value; + } else { + throw new Error("Not implemented: runtimeValue type " + node.type); + } + } + + provide(node) { + if(join.isVariable(node)) { + if(this.nonProviding && !this.myRegisters[node.id]) { + return; + } + this.unprovided[node.id] = false; + } + } + + setGroup(variable, value) { + let group = this.variableToGroup[variable.name] = value; + return group; + } + + getGroup(variable, orValue?) { + let group = this.variableToGroup[variable.name]; + if(group === undefined) { + group = this.setGroup(variable, orValue !== undefined ? orValue : this.groupIx++); + } + return group; + } + + hasVariable(variable) { + return this.variableToGroup[variable.name] !== undefined; + } + + assignGroups(block) { + let finished = false; + while(!finished) { + finished = true; + for(let equality of block.equalities) { + if(equality === undefined) continue; + let [left, right] = equality; + + if(left.type === "constant" && right.type === "constant") { + // these must be equal, otherwise this query doesn't make any sense + if(left.value !== right.value) { + this.errors.push(errors.incompatabileConstantEquality(block, left, right)); + } + } else if(left.type === "constant") { + let rightGroup = this.getGroup(right); + let rightValue = this.groupToValue[rightGroup]; + // if this is a variable, it came from a parent context and we can't just overwrite it in this case, + // the builder handles this case for us by adding explicit equality checks into the scans + if(!join.isVariable(rightValue)) { + if(rightValue !== undefined && left.value !== rightValue) { + this.errors.push(errors.incompatabileVariableToConstantEquality(block, right, rightValue, left)); + } + this.groupToValue[rightGroup] = left.value; + } + } else if(right.type === "constant") { + let leftGroup = this.getGroup(left); + let leftValue = this.groupToValue[leftGroup]; + // if this is a variable, it came from a parent context and we can't just overwrite it in this case, + // the builder handles this case for us by adding explicit equality checks into the scans + if(!join.isVariable(leftValue)) { + if(leftValue !== undefined && leftValue !== right.value) { + this.errors.push(errors.incompatabileVariableToConstantEquality(block, left, leftValue, right)); + } + this.groupToValue[leftGroup] = right.value; + } + } else { + let leftGroup = this.getGroup(left); + let rightGroup = this.getGroup(right, leftGroup); + if(leftGroup !== rightGroup) { + if(leftGroup < rightGroup) { + this.setGroup(right, leftGroup); + } else { + this.setGroup(left, rightGroup); + } + finished = false; + } + } + } + } + } + + assignRuntimeVariables(block) { + let registerToVars = this.registerToVars; + let groupToValue = this.groupToValue; + for(let varName in block.variables) { + let variable = block.variables[varName]; + let group = this.getGroup(variable); + if(group !== undefined) { + let value = groupToValue[group]; + if(value === undefined) { + if(variable.constant) { + value = variable.constant.value; + } else { + value = this.createVariable(); + registerToVars[value.id].push(varName); + } + groupToValue[group] = value; + } else { + if(variable.constant) { + if(!join.isVariable(value) && variable.constant.value !== value) { + this.errors.push(errors.incompatabileTransitiveEquality(block, variable, value)); + } + value = variable.constant.value; + if(this.myRegisters[value.id]) { + groupToValue[group] = value; + } + } else if(join.isVariable(value)) { + registerToVars[value.id].push(varName); + } + } + } + } + + let unprovided = this.unprovided; + for(let ix = 0; ix < this.varIx; ix++) { + if(unprovided[ix] === undefined && this.myRegisters[ix]) { + unprovided[ix] = true; + } + } + } + + createVariable() { + this.registerToVars[this.varIx] = []; + this.myRegisters[this.varIx] = true; + return new join.Variable(this.varIx++); + } + + extendTo(block) { + let neue = new BuilderContext(block, clone(this.variableToGroup), clone(this.groupToValue), this.unprovided, this.registerToVars, this.groupIx, this.varIx); + neue.errors = this.errors; + return neue; + } +} + +//----------------------------------------------------------- +// Scans +//----------------------------------------------------------- + +function checkBlockForVariable(block, variableName) { + let curBlock = block; + while(curBlock) { + let found = curBlock.variables[variableName]; + if(found) return found; + curBlock = curBlock.parent; + } + return; +} + +function checkSubBlockEqualities(context, block) { + // if we have an equality that is with a constant, then we need to add + // a node for that equality since we couldn't fold the constant into the variable + let equalityIx = 0; + for(let equality of block.equalities) { + if(!equality) continue; + let [left, right] = equality; + let needsEquality; + let hasLeft = context.hasVariable(left); + let hasRight = context.hasVariable(right); + if(left.type === "constant" && (hasRight || right.type === "constant")) { + needsEquality = true; + } else if(right.type === "constant" && (hasLeft || left.type === "constant")) { + needsEquality = true; + } else if(hasLeft && hasRight) { + needsEquality = true; + } else if(hasLeft && !join.isVariable(context.getValue(left))) { + needsEquality = true; + } else if(hasRight && !join.isVariable(context.getValue(right))) { + needsEquality = true; + } + // console.log("branch equality", left, right, leftVal, rightVal); + if(needsEquality) { + let expression = {type: "expression", op: "=", args: equality}; + block.expressions.push(expression) + block.equalities[equalityIx] = undefined; + } + equalityIx++; + } +} + +function buildScans(block, context, scanLikes, outputScans) { + let {unprovided} = block; + for(let scanLike of scanLikes) { + if(scanLike.type === "record") { + let entity = context.getValue(scanLike.variable); + context.provide(entity); + for(let attribute of scanLike.attributes) { + if(attribute.value.type === "parenthesis") { + for(let item of attribute.value.items) { + let value = context.getValue(item) + context.provide(value); + let final = new join.Scan(item.id + "|build", entity, attribute.attribute, value, undefined, scanLike.scopes); + outputScans.push(final); + item.buildId = final; + } + } else { + let value = context.getValue(attribute.value) + context.provide(value); + let final = new join.Scan(attribute.id + "|build", entity, attribute.attribute, value, undefined, scanLike.scopes); + outputScans.push(final); + attribute.buildId = final.id; + } + } + } else if(scanLike.type === "scan") { + let entity; + if(scanLike.entity) { + entity = context.getValue(scanLike.entity); + } + if(!scanLike.needsEntity) { + context.provide(entity); + } + let attribute; + if(scanLike.attribute) { + attribute = context.getValue(scanLike.attribute); + context.provide(attribute); + } + let value; + if(scanLike.value) { + value = context.getValue(scanLike.value) + context.provide(value); + } + let node; + if(scanLike.node) { + node = context.getValue(scanLike.node) + context.provide(node); + } + let final = new join.Scan(scanLike.id + "|build", entity, attribute, value, node, scanLike.scopes); + outputScans.push(final); + scanLike.buildId = final.id; + } else if(scanLike.type === "not") { + checkSubBlockEqualities(context, scanLike); + + let notContext = context.extendTo(scanLike); + notContext.nonProviding = true; + + let args = []; + let seen = []; + for(let variableName in scanLike.variables) { + let cur = checkBlockForVariable(block, variableName); + if(!cur) continue; + let value = notContext.getValue(cur); + if(join.isVariable(value)) { + seen[value.id] = true; + args.push(value); + } + } + let {strata} = buildStrata(scanLike, notContext); + let final = new join.NotScan(scanLike.id + "|build", args, strata); + outputScans.push(final); + scanLike.buildId = final.id; + } else if(scanLike.type === "ifExpression") { + let seen = []; + let args = []; + let branches = []; + let hasAggregate = false; + for(let variable of scanLike.outputs) { + let value = context.getValue(variable); + if(join.isVariable(value)) { + seen[value.id] = true; + } + } + for(let branch of scanLike.branches) { + checkSubBlockEqualities(context, branch.block); + + let branchContext = context.extendTo(branch.block); + for(let variableName in branch.block.variables) { + let cur = checkBlockForVariable(branch.block.parent, variableName); + if(!cur) continue; + let value = branchContext.getValue(cur); + if(join.isVariable(value) && !seen[value.id]) { + seen[value.id] = true; + args.push(value); + } + } + let {strata} = buildStrata(branch.block, branchContext); + let outputs = []; + for(let output of branch.outputs) { + outputs.push(branchContext.getValue(output)); + } + if(strata.length > 1) { + hasAggregate = true; + } + let final = new join.IfBranch(branch.id + "|build", strata, outputs, branch.exclusive); + branches.push(final); + branch.buildId = final.id; + } + let outputs = []; + for(let output of scanLike.outputs) { + let resolved = context.getValue(output); + if(!join.isVariable(resolved)) { + let variable = context.createVariable(); + let impl = providers.get("="); + outputScans.push(new impl(`${output.id}|equality|build`, [variable, resolved], [])); + outputs.push(variable); + context.provide(variable); + } else { + outputs.push(resolved); + context.provide(resolved); + } + } + let ifScan = new join.IfScan(scanLike.id + "|build", args, outputs, branches, hasAggregate); + outputScans.push(ifScan) + scanLike.buildId = ifScan.id; + } else { + throw new Error("Not implemented: scanLike " + scanLike.type); + } + } + return outputScans; +} + +//----------------------------------------------------------- +// Expressions +//----------------------------------------------------------- + +function buildExpressions(block, context, expressions, outputScans) { + for(let expression of expressions) { + if(expression.type === "expression") { + let results = []; + if(expression.variable) { + let result = context.getValue(expression.variable); + results.push(result); + context.provide(result); + } + let args = []; + for(let arg of expression.args) { + args.push(context.getValue(arg)); + } + let impl = providers.get(expression.op); + if(impl) { + outputScans.push(new impl(`${expression.id}|build`, args, results)); + } else { + context.errors.push(errors.unimplementedExpression(block, expression)); + } + } else if(expression.type === "functionRecord") { + let results; + if(expression.returns !== undefined) { + results = expression.returns.slice(); + } else { + results = [expression.variable]; + let resolved = context.getValue(expression.variable); + context.provide(resolved); + } + let args = []; + let impl = providers.get(expression.op); + if(!impl) { + context.errors.push(errors.unimplementedExpression(block, expression)); + return; + } + for(let attribute of expression.record.attributes) { + let ix = impl.AttributeMapping[attribute.attribute]; + if(ix !== undefined) { + args[ix] = context.getValue(attribute.value); + } else if(impl.ReturnMapping && (ix = impl.ReturnMapping[attribute.attribute]) !== undefined) { + results[ix] = attribute.value; + } else { + // @TODO: error - unknown arg/return for the function call + } + } + let resultIx = 0; + for(let result of results) { + // if one of the returns is fixed, we need to add an equality check + // to make sure that the return is actually that constant. The constraint + // provider may be smart enough to do that themselves, but this removes + // the burden from them. + let resolved = context.getValue(result); + context.provide(resolved); + results[resultIx] = resolved; + if(!join.isVariable(resolved)) { + // @TODO: mark this variable as generated? + let variable = context.createVariable(); + let klass = providers.get("="); + outputScans.push(new klass(`${resolved}|${resultIx}|equality|build`, [variable, resolved], [])) + resolved = results[resultIx] = variable; + } + resultIx++; + } + + outputScans.push(new impl(`${expression.id}|build`, args, results)); + } else { + throw new Error("Not implemented: function type " + expression.type); + } + } + return outputScans; +} + +//----------------------------------------------------------- +// Actions +//----------------------------------------------------------- + +function buildActions(block, context, actions, scans) { + let {unprovided} = context; + let actionObjects = []; + for(let action of actions) { + if(action.type === "record") { + let projection = []; + if(action.extraProjection) { + for(let proj of action.extraProjection) { + let variable = context.getValue(proj); + projection[variable.id] = variable; + } + } + let entity = context.getValue(action.variable); + for(let attribute of action.attributes) { + let impl; + if(action.action === "<-") { + impl = ActionImplementations[":="]; + // doing foo <- [#bar] shouldn't remove all the other tags that record has + // same for names + if(attribute.attribute === "name" || attribute.attribute === "tag") { + impl = ActionImplementations["+="]; + } + } else { + impl = ActionImplementations[action.action]; + } + if(attribute.value.type === "parenthesis") { + for(let item of attribute.value.items) { + let value = context.getValue(item) + if(value instanceof join.Variable) { + if(!attribute.nonProjecting && !attribute.value.nonProjecting && !item.nonProjecting) { + projection[value.id] = value; + } + } + let final = new impl(`${attribute.id}|${item.id}|build`, entity, attribute.attribute, value, undefined, action.scopes); + actionObjects.push(final); + item.buildId = final.id; + } + } else { + let value = context.getValue(attribute.value) + if(value instanceof join.Variable) { + if(!attribute.nonProjecting && !attribute.value.nonProjecting) { + projection[value.id] = value; + } + } + let final = new impl(`${attribute.id}|build`, entity, attribute.attribute, value, undefined, action.scopes); + actionObjects.push(final); + attribute.buildId = final.id; + } + } + // if this variable is unprovided, we need to generate an id + if(unprovided[entity.id]) { + projection = projection.filter((x) => x); + let klass = providers.get("generateId"); + scans.push(new klass(`${action.id}|${entity.id}|build`, projection, [entity])); + context.provide(entity); + } + } else if(action.type === "action") { + let {entity, value, attribute} = action; + let impl = ActionImplementations[action.action]; + if(action.action === "erase") { + let attributeValue = attribute && attribute.type !== undefined ? context.getValue(attribute) : attribute; + let final = new impl(`${action.id}|build`, context.getValue(entity), attributeValue, undefined, undefined, action.scopes); + actionObjects.push(final); + action.buildId = final.id; + } else { + if(entity === undefined || value === undefined || attribute === undefined) { + context.errors.push(errors.invalidLookupAction(block, action)); + continue; + } + attribute = typeof attribute === "string" ? attribute : context.getValue(attribute); + if(value.type === "parenthesis") { + for(let item of value.items) { + let final = new impl(`${action.id}|${item.id}|build`, context.getValue(entity), attribute, context.getValue(item), undefined, action.scopes); + actionObjects.push(final); + item.buildId = final.id; + } + } else { + let final = new impl(`${action.id}|build`, context.getValue(entity), attribute, context.getValue(value), undefined, action.scopes); + actionObjects.push(final); + action.buildId = final.id; + } + } + // throw new Error("Action actions aren't implemented yet.") + } else { + throw new Error("Not implemented: action " + action.type); + } + } + return actionObjects; +} + +//----------------------------------------------------------- +// Stratifier +//----------------------------------------------------------- + +function stratify(scans) { + if(!scans.length) return [new BlockStratum([], [])]; + + let variableInfo = {}; + let blockLevel = {}; + + let provide = (variable, scan) => { + if(join.isVariable(variable)) { + let info = variableInfo[variable.id] + if(!info) { + info = variableInfo[variable.id] = {providers: []}; + } + info.providers.push(scan); + } + } + + let maybeLevelVariable = (scan, level, variable) => { + if(join.isVariable(variable)) { + let info = variableInfo[variable.id] + let minLevel = level; + for(let provider of info.providers) { + let providerLevel = blockLevel[scan.id] || 0; + minLevel = Math.min(minLevel, providerLevel); + } + info.level = level; + } + } + + for(let scan of scans) { + if(scan instanceof join.Scan) { + provide(scan.e, scan); + provide(scan.a, scan); + provide(scan.v, scan); + } else if(scan instanceof Aggregate || scan instanceof Sort) { + for(let ret of scan.returns) { + provide(ret, scan); + blockLevel[scan.id] = 1; + if(join.isVariable(ret)) { + variableInfo[ret.id].level = 1; + } + } + } else if(scan instanceof join.Constraint) { + for(let ret of scan.returns) { + provide(ret, scan); + } + } else if(scan instanceof join.IfScan) { + for(let output of scan.outputs) { + provide(output, scan); + } + } else if(scan instanceof join.NotScan) { + // not can never provide a variable, so there's nothing + // we need to do here + } + } + + let round = 0; + let changed = true; + while(changed && round <= scans.length) { + changed = false + // for each scan, get the max level of the variables you rely on + // if it's greater than your current level, set your level to that. + // Now check all of the scans vars and see if you are either the only + // provider or if all the providers are now in a higher level. If so, + // the variable's level is set to the scan's new level. + for(let scan of scans) { + let isAggregate = false; + if(scan instanceof Aggregate || + scan instanceof Sort || + scan.hasAggregate || + (scan.strata && scan.strata.length > 1)) { + isAggregate = true; + } + + let levelMax = 0; + let scanLevel = blockLevel[scan.id] || 0; + let dependentVariables; + let returnVariables; + if(scan instanceof join.Scan) { + dependentVariables = scan.vars; + returnVariables = scan.vars; + } else if(scan.args !== undefined) { + dependentVariables = scan.args; + returnVariables = scan.returns || scan.outputs; + } else { + throw new Error("Scan that I don't know how to stratify: " + scan) + } + + for(let variable of dependentVariables) { + if(join.isVariable(variable)) { + let info = variableInfo[variable.id]; + let infoLevel = 0; + if(info && info.level) { + infoLevel = info.level + } + // if this is an aggregate, we always have to be in the level that is + // one greater than all our dependencies + if(isAggregate) { + infoLevel += 1; + } + levelMax = Math.max(levelMax, infoLevel); + } + } + + if(levelMax > scanLevel) { + changed = true; + blockLevel[scan.id] = levelMax; + if(returnVariables) { + for(let variable of returnVariables) { + maybeLevelVariable(scan, levelMax, variable); + } + } + } + } + round++; + } + + if(round > scans.length) { + throw new Error("Stratification cycle"); + } + + let strata = [{scans: [], aggregates: []}]; + for(let scan of scans) { + let scanStratum = blockLevel[scan.id]; + if(scanStratum !== undefined) { + let level = strata[scanStratum]; + if(!level) level = strata[scanStratum] = {scans: [], aggregates: []}; + if(scan instanceof Aggregate || scan instanceof Sort) { + level.aggregates.push(scan); + } + level.scans.push(scan); + } else { + strata[0].scans.push(scan); + } + } + // console.log(inspect(strata, {colors: true, depth: 10})); + + let built = []; + for(let level of strata) { + if(level) { + built.push(new BlockStratum(level.scans, level.aggregates)); + } + } + + return built; +} + +function buildStrata(block, context: BuilderContext) { + let scans = []; + buildExpressions(block, context, block.expressions, scans); + buildScans(block, context, block.scanLike, scans); + + let binds = buildActions(block, context, block.binds, scans); + let commits = buildActions(block, context, block.commits, scans); + + let strata = stratify(scans); + + return {strata, binds, commits}; +} + +//----------------------------------------------------------- +// Block and Doc +//----------------------------------------------------------- + +export function buildBlock(block) { + let context = new BuilderContext(block); + let {strata, binds, commits} = buildStrata(block, context); + + // console.log("-- scans ----------------------------------------------------------------"); + // console.log(inspect(scans, {colors: true, depth: 10})); + + // console.log("-- binds ----------------------------------------------------------------"); + // console.log(inspect(binds, {colors: true})); + + // console.log("-- commits --------------------------------------------------------------"); + // console.log(inspect(commits, {colors: true})); + + let ix = 0; + for(let unprovided of context.unprovided) { + let vars = context.registerToVars[ix].map((varName) => block.variableLookup[varName]); + if(unprovided) { + context.errors.push(errors.unprovidedVariableGroup(block, vars)); + } + for(let variable of vars) { + variable.register = ix; + } + ix++; + } + + return { + block: new Block(block.name || "Unnamed block", strata, commits, binds, block), + errors: context.errors, + }; +} + +export function buildDoc(parsedDoc) { + let blocks = []; + let setupInfos = []; + let allErrors = []; + for(let parsedBlock of parsedDoc.blocks) { + let {block, errors} = buildBlock(parsedBlock); + if(errors.length) { + for(let error of errors) { + allErrors.push(error); + } + } else { + blocks.push(block); + } + } + return { blocks, errors: allErrors }; +} diff --git a/src/runtime/changes.ts b/src/runtime/changes.ts new file mode 100644 index 000000000..d16ea775b --- /dev/null +++ b/src/runtime/changes.ts @@ -0,0 +1,306 @@ +//--------------------------------------------------------------------- +// Changes +//--------------------------------------------------------------------- + +import {MultiIndex} from "./indexes"; + +let perf = global["perf"]; + +//--------------------------------------------------------------------- +// ChangeType +//--------------------------------------------------------------------- + +export enum ChangeType { + ADDED, + REMOVED, + ADDED_REMOVED, +} + +//--------------------------------------------------------------------- +// ChangesIndex +//--------------------------------------------------------------------- + +export class ChangesIndex { + pos: number; + positions: any; + info: any[]; + constructor() { + this.positions = {}; + this.info = []; + this.pos = 0; + } + + store(scope,e,a,v,node,key?) { + // let start = perf.time() + key = key || `${scope}|${e}|${a}|${v}|${node}`; + let keyPos = this.positions[key]; + let info = this.info; + if(keyPos === undefined) { + let pos = this.pos; + this.positions[key] = pos; + info[pos] = ChangeType.ADDED; + info[pos + 1] = e; + info[pos + 2] = a; + info[pos + 3] = v; + info[pos + 4] = node; + info[pos + 5] = scope; + this.pos += 6; + } else if(info[keyPos] === ChangeType.REMOVED) { + info[keyPos] = ChangeType.ADDED_REMOVED; + } + // perf.store(start); + return key; + } + unstore(scope,e,a,v,node,key?) { + key = key || `${scope}|${e}|${a}|${v}|${node}`; + let keyPos = this.positions[key]; + let info = this.info; + if(keyPos === undefined) { + let pos = this.pos; + this.positions[key] = pos; + info[pos] = ChangeType.REMOVED; + info[pos + 1] = e; + info[pos + 2] = a; + info[pos + 3] = v; + info[pos + 4] = node; + info[pos + 5] = scope; + this.pos += 6; + } else if(info[keyPos] === ChangeType.ADDED) { + info[keyPos] = ChangeType.ADDED_REMOVED; + } + return key; + } + + inc(scope, e,a,v,node,key?) { + key = key || `${scope}|${e}|${a}|${v}|${node}`; + let keyPos = this.positions[key]; + let info = this.info; + if(keyPos === undefined) { + let pos = this.pos; + this.positions[key] = pos; + info[pos] = 1; + info[pos + 1] = e; + info[pos + 2] = a; + info[pos + 3] = v; + info[pos + 4] = node; + info[pos + 5] = scope; + this.pos += 6; + } else { + info[keyPos] += 1; + } + return key; + } + dec(scope,e,a,v,node,key?) { + key = key || `${scope}|${e}|${a}|${v}|${node}`; + let keyPos = this.positions[key]; + let info = this.info; + if(keyPos === undefined) { + let pos = this.pos; + this.positions[key] = pos; + info[pos] = -1; + info[pos + 1] = e; + info[pos + 2] = a; + info[pos + 3] = v; + info[pos + 4] = node; + info[pos + 5] = scope; + this.pos += 6; + } else { + info[keyPos] -= 1; + } + return key; + } +} + +//--------------------------------------------------------------------- +// Changes +//--------------------------------------------------------------------- + +export class Changes { + round: number; + changed: boolean; + index: MultiIndex; + changes: any[]; + finalChanges: ChangesIndex; + capturedChanges: any; + + constructor(index: MultiIndex) { + this.index = index; + this.round = 0; + this.changed = false; + this.changes = [new ChangesIndex()]; + this.finalChanges = new ChangesIndex(); + } + + capture() { + this.capturedChanges = new ChangesIndex(); + } + + captureEnd() { + let cur = this.capturedChanges; + this.capturedChanges = undefined; + return cur; + } + + store(scope, e,a,v,node?) { + // console.log("STORING", e, a, v, node, this.index.lookup(e,a,v,node) === undefined); + let key = this.changes[this.round].store(scope,e,a,v,node); + let captured = this.capturedChanges; + if(captured !== undefined) { + captured.store(scope,e,a,v,node,key); + } + } + + unstore(scope, e,a,v,node?) { + // console.log("REMOVING", e, a, v, node, this.index.lookup(e,a,v,node) === undefined); + if(node === undefined) { + //multidb + let level = this.index.getIndex(scope).lookup(e,a,v); + if(level) { + let index = level.index; + for(let key of Object.keys(index)) { + let nodeValue = index[key]; + this.unstore(scope,e,a,v,nodeValue); + } + } + } else { + let key = this.changes[this.round].unstore(scope,e,a,v,node); + let captured = this.capturedChanges; + if(captured !== undefined) { + captured.unstore(scope, e,a,v,node,key); + } + } + } + + commit() { + let final = this.finalChanges; + let changes = this.changes[this.round]; + let {info, positions} = changes; + let keys = Object.keys(positions); + let multiIndex = this.index; + let committed = []; + let committedIx = 0; + for(let key of keys) { + let pos = positions[key]; + let mult = info[pos]; + if(mult === ChangeType.ADDED_REMOVED) { + continue; + } + let e = info[pos + 1]; + let a = info[pos + 2]; + let v = info[pos + 3]; + let node = info[pos + 4]; + let scope = info[pos + 5]; + let curIndex = multiIndex.getIndex(scope); + if(mult === ChangeType.REMOVED && curIndex.lookup(e,a,v,node) !== undefined) { + this.changed = true; + curIndex.unstore(e,a,v,node); + final.dec(scope,e,a,v,node,key); + committed[committedIx] = ChangeType.REMOVED; + committed[committedIx+1] = e; + committed[committedIx+2] = a; + committed[committedIx+3] = v; + committed[committedIx+4] = node; + committed[committedIx+5] = scope; + committedIx += 6; + } else if(mult === ChangeType.ADDED && curIndex.lookup(e,a,v,node) === undefined) { + this.changed = true; + curIndex.store(e,a,v,node); + final.inc(scope,e,a,v,node,key); + committed[committedIx] = ChangeType.ADDED; + committed[committedIx+1] = e; + committed[committedIx+2] = a; + committed[committedIx+3] = v; + committed[committedIx+4] = node; + committed[committedIx+5] = scope; + committedIx += 6; + } + } + return committed; + } + + nextRound() { + this.round++; + this.changed = false; + this.changes[this.round] = new ChangesIndex(); + } + + + toCommitted(scopeLookup: Object) { + let commit = []; + let ix = 0; + let {positions, info} = this.finalChanges; + let indexes = this.index.indexes; + let keys = Object.keys(positions); + for(let key of keys) { + let pos = positions[key]; + let count = info[pos]; + if(count === 0) continue; + let scope = info[pos + 5]; + if(scopeLookup && !scopeLookup[scope]) continue; + + let action = count > 1 ? ChangeType.ADDED : ChangeType.REMOVED + let e = info[pos + 1]; + let a = info[pos + 2]; + let v = info[pos + 3]; + let node = info[pos + 4]; + + commit[ix] = action; + commit[ix + 1] = e; + commit[ix + 2] = a; + commit[ix + 3] = v; + commit[ix + 4] = node; + commit[ix + 5] = scope; + ix += 6; + } + return commit; + } + + result(scopeLookup?: Object) { + let insert = []; + let remove = []; + let {positions, info} = this.finalChanges; + let indexes = this.index.indexes; + let keys = Object.keys(positions); + for(let key of keys) { + let pos = positions[key]; + let count = info[pos]; + let e = info[pos + 1]; + let a = info[pos + 2]; + let v = info[pos + 3]; + let scope = info[pos + 5]; + if(scopeLookup === undefined || scopeLookup[scope]) { + if(count < 0 && indexes[scope].lookup(e,a,v) === undefined) { + remove.push([e,a,v]); + } else if(count > 0 && indexes[scope].lookup(e,a,v) !== undefined) { + insert.push([e,a,v]); + } + } + } + return {type: "result", insert, remove}; + } + + _storeObject(operation: "store" | "unstore", id: string, object: any, node: string, scope: string) { + for(let attr of Object.keys(object)) { + let value = object[attr]; + if(value === undefined) continue; + if(value.constructor === Array) { + for(let item of value) { + this[operation](scope, id, attr, item, node); + } + } else if(typeof value === "object") { + throw new Error("Attempting to store a non-value in an Eve database"); + } else { + this[operation](scope, id, attr, value, node); + } + } + } + + storeObject(id: string, object: any, node: string, scope: string) { + this._storeObject("store", id, object, node, scope); + } + + unstoreObject(id: string, object: any, node: string, scope: string) { + this._storeObject("unstore", id, object, node, scope); + } +} + diff --git a/src/runtime/databases/browserSession.ts b/src/runtime/databases/browserSession.ts new file mode 100644 index 000000000..8461cbae2 --- /dev/null +++ b/src/runtime/databases/browserSession.ts @@ -0,0 +1,98 @@ +//--------------------------------------------------------------------- +// Browser Session Database +//--------------------------------------------------------------------- + +import * as parser from "../parser"; +import * as builder from "../builder"; +import {InsertAction, SetAction} from "../actions"; +import {Changes} from "../changes"; +import {Evaluation, Database} from "../runtime"; +import * as eveSource from "../eveSource"; + +interface BrowserClient { + send(json: string); +} + +export class BrowserEventDatabase extends Database { + constructor() { + super(); + let source = eveSource.get("event.eve"); + if(source) { + let {results, errors} = parser.parseDoc(source, "event"); + if(errors && errors.length) console.error("EVENT ERRORS", errors); + let {blocks, errors: buildErrors} = builder.buildDoc(results); + if(buildErrors && buildErrors.length) console.error("EVENT ERRORS", buildErrors); + this.blocks = blocks; + } + } +} + +export class BrowserViewDatabase extends Database { + constructor() { + super(); + let source = eveSource.get("view.eve"); + if(source) { + let {results, errors} = parser.parseDoc(source, "view"); + if(errors && errors.length) console.error("View DB Errors", errors); + let {blocks, errors: buildErrors} = builder.buildDoc(results); + if(buildErrors && buildErrors.length) console.error("View DB Errors", buildErrors); + this.blocks = blocks; + } + } +} + +export class BrowserEditorDatabase extends Database { + constructor() { + super(); + let source = eveSource.get("editor.eve"); + if(source) { + let {results, errors} = parser.parseDoc(source, "editor"); + if(errors && errors.length) console.error("Editor DB Errors", errors); + let {blocks, errors: buildErrors} = builder.buildDoc(results); + if(buildErrors && buildErrors.length) console.error("Editor DB Errors", buildErrors); + this.blocks = blocks; + } + } +} + +export class BrowserInspectorDatabase extends Database { + constructor() { + super(); + let source = eveSource.get("inspector.eve"); + if(source) { + let {results, errors} = parser.parseDoc(source, "inspector"); + if(errors && errors.length) console.error("Inspector DB Errors", errors); + let {blocks, errors: buildErrors} = builder.buildDoc(results); + if(buildErrors && buildErrors.length) console.error("Inspector DB Errors", buildErrors); + this.blocks = blocks; + } + } +} + +export class BrowserSessionDatabase extends Database { + client: BrowserClient; + + constructor(client: BrowserClient) { + super(); + this.client = client; + } + + onFixpoint(evaluation: Evaluation, changes: Changes) { + super.onFixpoint(evaluation, changes); + let name = evaluation.databaseToName(this); + let result = changes.result({[name]: true}); + if(result.insert.length || result.remove.length) { + this.client.send(JSON.stringify(result)); + } + } + + unregister(evaluation: Evaluation) { + let ix = this.evaluations.indexOf(evaluation); + if(ix > -1) { + this.evaluations.splice(ix, 1); + } + if(this.evaluations.length === 0) { + this.client.send(JSON.stringify({type: "result", insert: [], remove: this.index.toTriples()})) + } + } +} diff --git a/src/runtime/databases/http.ts b/src/runtime/databases/http.ts new file mode 100644 index 000000000..f8e848f50 --- /dev/null +++ b/src/runtime/databases/http.ts @@ -0,0 +1,80 @@ +//--------------------------------------------------------------------- +// Http Database +//--------------------------------------------------------------------- + +import {InsertAction} from "../actions" +import {Changes} from "../changes"; +import {Evaluation, Database} from "../runtime"; +import * as eavs from "../util/eavs"; + +export class HttpDatabase extends Database { + + sendRequest(evaluation, requestId, request) { + var oReq = new XMLHttpRequest(); + oReq.addEventListener("load", () => { + let body = oReq.responseText; + let scope = "http"; + let responseId = `${requestId}|response`; + let changes = evaluation.createChanges(); + changes.store(scope, requestId, "response", responseId, this.id); + changes.store(scope, responseId, "tag", "response", this.id); + changes.store(scope, responseId, "body", body, this.id); + let contentType = oReq.getResponseHeader("content-type"); + if(contentType && contentType.indexOf("application/json") > -1 && body) { + let id = eavs.fromJS(changes, JSON.parse(body), this.id, scope, `${responseId}|json`); + changes.store(scope, responseId, "json", id, this.id); + } + evaluation.executeActions([], changes); + }); + let method = "GET"; + if(request.method) { + method = request.method[0]; + } + + oReq.open(method, request.url[0]); + + if(request.headers) { + let headers = this.index.asObject(request.headers[0]); + for(let header in headers) { + oReq.setRequestHeader(header, headers[header][0]); + } + } + + if(request.body) { + oReq.send(request.body[0]); + } else if(request.json) { + let object = this.index.asObject(request.json[0], true, true); + oReq.setRequestHeader("Content-Type", "application/json"); + oReq.send(JSON.stringify(object)); + } else { + oReq.send(); + } + } + + onFixpoint(evaluation: Evaluation, changes: Changes) { + let name = evaluation.databaseToName(this); + let result = changes.result({[name]: true}); + let handled = {}; + let index = this.index; + let actions = []; + for(let insert of result.insert) { + let [e,a,v] = insert; + if(!handled[e]) { + handled[e] = true; + if(index.lookup(e,"tag", "request") && !index.lookup(e, "tag", "sent")) { + let request = index.asObject(e); + if(request.url) { + actions.push(new InsertAction("http|sender", e, "tag", "sent", undefined, [name])); + this.sendRequest(evaluation, e, request); + } + } + } + } + if(actions.length) { + setTimeout(() => { + // console.log("actions", actions); + evaluation.executeActions(actions); + }) + } + } +} diff --git a/src/runtime/databases/node/http.ts b/src/runtime/databases/node/http.ts new file mode 100644 index 000000000..aed0c9087 --- /dev/null +++ b/src/runtime/databases/node/http.ts @@ -0,0 +1,77 @@ +//--------------------------------------------------------------------- +// Node HTTP Database +//--------------------------------------------------------------------- + +import {InsertAction} from "../../actions" +import {Changes} from "../../changes"; +import {Evaluation, Database} from "../../runtime"; +import * as eavs from "../../util/eavs"; +import * as httpRequest from "request"; + +export class HttpDatabase extends Database { + + sendRequest(evaluation, requestId, request) { + let options: any = {url: request.url[0], headers: {}}; + if(request.headers) { + let headers = this.index.asObject(request.headers[0]); + for(let header in headers) { + options.headers[header] = headers[header]; + } + } + if(request.method) { + options.method = request.method[0]; + } + if(request.json) { + let object = this.index.asObject(request.json[0], true, true); + options.headers["Content-Type"] = "application/json"; + options.body = JSON.stringify(object); + } + if(request.body) { + options.body = request.body[0]; + } + httpRequest(options, (error, response, body) => { + // console.log("GOT RESPONSE", response.statusCode); + // console.log(error); + // console.log(response); + // console.log(body); + let scope = "http"; + let responseId = `${requestId}|response`; + let changes = evaluation.createChanges(); + changes.store(scope, requestId, "response", responseId, this.id); + changes.store(scope, responseId, "tag", "response", this.id); + if(response.headers["content-type"].indexOf("application/json") > -1) { + let id = eavs.fromJS(changes, JSON.parse(body), this.id, scope, `${responseId}|json`); + changes.store(scope, responseId, "json", id, this.id); + } + changes.store(scope, responseId, "body", body, this.id); + evaluation.executeActions([], changes); + }) + } + + onFixpoint(evaluation: Evaluation, changes: Changes) { + let name = evaluation.databaseToName(this); + let result = changes.result({[name]: true}); + let handled = {}; + let index = this.index; + let actions = []; + for(let insert of result.insert) { + let [e,a,v] = insert; + if(!handled[e]) { + handled[e] = true; + if(index.lookup(e,"tag", "request") && !index.lookup(e, "tag", "sent")) { + let request = index.asObject(e); + if(request.url) { + actions.push(new InsertAction("http|sender", e, "tag", "sent", undefined, [name])); + this.sendRequest(evaluation, e, request); + } + } + } + } + if(actions.length) { + process.nextTick(() => { + evaluation.executeActions(actions); + }) + } + } +} + diff --git a/src/runtime/databases/node/server.ts b/src/runtime/databases/node/server.ts new file mode 100644 index 000000000..f688d251e --- /dev/null +++ b/src/runtime/databases/node/server.ts @@ -0,0 +1,112 @@ +//--------------------------------------------------------------------- +// Node Server Database +//--------------------------------------------------------------------- + +import {InsertAction} from "../../actions" +import {Changes} from "../../changes"; +import {Evaluation, Database} from "../../runtime"; +import * as request from "request"; + +export class ServerDatabase extends Database { + + handling: boolean; + receiving: boolean; + requestId: number; + requestToResponse: any; + + constructor() { + super(); + this.handling = false; + this.requestId = 0; + this.receiving = false; + this.requestToResponse = {}; + } + + handleHttpRequest(request, response) { + if(!this.receiving) return; + + let scopes = ["server"]; + let requestId = `request|${this.requestId++}|${(new Date()).getTime()}` + this.requestToResponse[requestId] = response; + let actions = [ + new InsertAction("server|tag", requestId, "tag", "request", undefined, scopes), + new InsertAction("server|url", requestId, "url", request.url, undefined, scopes), + ]; + if(request.headers) { + let headerId = `${requestId}|body`; + for(let key of Object.keys(request.headers)) { + actions.push(new InsertAction("server|header", headerId, key, request.headers[key], undefined, scopes)); + } + actions.push(new InsertAction("server|headers", requestId, "headers", headerId, undefined, scopes)) + } + if(request.body) { + let body = request.body; + if(typeof body === "string") { + // nothing we need to do + } else { + let bodyId = `${requestId}|body`; + for(let key of Object.keys(body)) { + actions.push(new InsertAction("server|request-body-entry", bodyId, key, body[key], undefined, scopes)); + } + body = bodyId; + } + actions.push(new InsertAction("server|request-body", requestId, "body", body, undefined, scopes)) + } + let evaluation = this.evaluations[0]; + evaluation.executeActions(actions); + } + + analyze(evaluation: Evaluation, db: Database) { + for(let block of db.blocks) { + for(let scan of block.parse.scanLike) { + if(scan.type === "record" && scan.scopes.indexOf("server") > -1) { + for(let attribute of scan.attributes) { + if(attribute.attribute === "tag" && attribute.value.value === "request") { + this.receiving = true; + } + } + } + } + } + } + + sendResponse(evaluation, requestId, status, body) { + let response = this.requestToResponse[requestId]; + response.statusCode = status; + response.end(body); + } + + onFixpoint(evaluation: Evaluation, changes: Changes) { + let name = evaluation.databaseToName(this); + let result = changes.result({[name]: true}); + let handled = {}; + let index = this.index; + let actions = []; + for(let insert of result.insert) { + let [e,a,v] = insert; + if(!handled[e]) { + handled[e] = true; + if(index.lookup(e,"tag", "request") && !index.lookup(e, "tag", "sent")) { + let responses = index.asValues(e, "response"); + if(responses || index.lookup(e, "tag", "handling")) this.handling = true; + if(responses === undefined) continue; + let [response] = responses; + let {status, body} = index.asObject(response); + actions.push(new InsertAction("server|sender", e, "tag", "sent", undefined, [name])); + this.sendResponse(evaluation, e, status[0], body[0]); + } + } + } + if(actions.length) { + process.nextTick(() => { + evaluation.executeActions(actions); + // because this database is created per http request, we need to destroy this + // evaluation once a response has been sent and we've dealt with any consequences + // of the send. + evaluation.close(); + }) + } + } +} + + diff --git a/src/runtime/databases/persisted.ts b/src/runtime/databases/persisted.ts new file mode 100644 index 000000000..0bf605a45 --- /dev/null +++ b/src/runtime/databases/persisted.ts @@ -0,0 +1,15 @@ +//--------------------------------------------------------------------- +// Persisted Database +//--------------------------------------------------------------------- + +import {Changes} from "../changes"; +import {Evaluation, Database} from "../runtime"; + +export class PersistedDatabase extends Database { + + onFixpoint(evaluation: Evaluation, changes: Changes) { + super.onFixpoint(evaluation, changes); + } + +} + diff --git a/src/runtime/databases/system.ts b/src/runtime/databases/system.ts new file mode 100644 index 000000000..ba87bc6d0 --- /dev/null +++ b/src/runtime/databases/system.ts @@ -0,0 +1,183 @@ +//--------------------------------------------------------------------- +// System database +//--------------------------------------------------------------------- + +import {InsertAction, SetAction} from "../actions" +import {Evaluation, Database} from "../runtime" + +//--------------------------------------------------------------------- +// Agents +//--------------------------------------------------------------------- + +class TimeAgent { + + static attributeOrdering = ["year", "month", "day", "hours", "hours-24", "ampm", "minutes", "time-string", "seconds", "timestamp", "frames"]; + static updateIntervals = { + "year": 1000 * 60 * 60, + "month": 1000 * 60 * 60, + "day": 1000 * 60 * 60, + "hours": 1000 * 60 * 60, + "hours-24": 1000 * 60 * 60, + "ampm": 1000 * 60 * 60, + "minutes": 1000 * 60, + "time-string": 1000 * 60, + "seconds": 1000, + "timestamp": 1000, + "frames": 16, + }; + + timeout: any; + interval: number; + frames: number; + constructor() { + this.frames = 0; + } + + configure(record) { + let max = this.interval || -1; + let interval = TimeAgent.updateIntervals["year"]; + for(let attribute of record.attributes) { + let attr = attribute.attribute; + let index = TimeAgent.attributeOrdering.indexOf(attr) + if(index > max) { + max = index; + interval = TimeAgent.updateIntervals[attr]; + } + } + this.interval = interval; + } + + timeActions() { + let time = new Date(); + this.frames++; + let ampm = time.getHours() >= 12 ? "PM" : "AM"; + let formattedMinutes = time.getMinutes() >= 10 ? time.getMinutes() : `0${time.getMinutes()}`; + let formattedHours = time.getHours() % 12 === 0 ? 12 : time.getHours() % 12; + let timeString = `${formattedHours}:${formattedMinutes} ${ampm}`; + return [ + new InsertAction("time|tag", "time", "tag", "time"), + new SetAction("time|year","time", "year", time.getFullYear()), + new SetAction("time|month","time", "month", time.getMonth()), + new SetAction("time|day","time", "day", time.getDate()), + new SetAction("time|hours","time", "hours", time.getHours() % 12), + new SetAction("time|hours-24","time", "hours-24", time.getHours()), + new SetAction("time|minutes","time", "minutes", time.getMinutes()), + new SetAction("time|time-string","time", "time-string", timeString), + new SetAction("time|seconds","time", "seconds", time.getSeconds()), + new SetAction("time|timestamp","time", "timestamp", time.getTime()), + new SetAction("time|frames","time", "frames", this.frames), + new SetAction("time|time","time", "ampm", ampm), + ]; + } + + run(evaluation: Evaluation) { + let self = this; + this.timeout = setInterval(function() { + evaluation.executeActions(self.timeActions()); + // self.run(evaluation); + }, this.interval); + } + + setup(evaluation: Evaluation) { + if(this.interval !== undefined) { + this.timeout = setTimeout(() => { + evaluation.executeActions(this.timeActions()); + this.run(evaluation); + }, 0) + } + } + + close() { + clearTimeout(this.timeout); + } +} + +class MemoryAgent { + + timeout: any; + interval: number; + os: any; + process: any; + + configure(record) { + // this.os = require("os"); + this.interval = 1000; + } + + memoryActions() { + + let {rss} = process.memoryUsage(); + return [ + new InsertAction("memory|tag", "memory", "tag", "memory"), + new SetAction("memory|rss","memory", "rss", rss), + ]; + } + + setup(evaluation: Evaluation) { + let self = this; + if(this.interval !== undefined) { + evaluation.executeActions(this.memoryActions()); + this.timeout = setInterval(function() { + evaluation.executeActions(self.memoryActions()); + }, this.interval); + } + } + + close() { + clearTimeout(this.timeout); + } +} + +class BrowserMemoryAgent { + timeout: any; + interval: number; + configure(record) { } + setup(evaluation: Evaluation) { } + close() { } +} + + +export class SystemDatabase extends Database { + time: any; + memory: any; + + analyze(evaluation: Evaluation, db: Database) { + let time; + let memory; + for(let block of db.blocks) { + for(let scan of block.parse.scanLike) { + if(scan.type === "record") { + for(let attribute of scan.attributes) { + if(attribute.attribute === "tag" && attribute.value.value === "time") { + if(this.time) this.time.close(); + time = this.time = new TimeAgent(); + time.configure(scan); + } else if(attribute.attribute === "tag" && attribute.value.value === "memory") { + if(this.memory) this.memory.close(); + if(global["browser"]) { + memory = this.memory = new BrowserMemoryAgent(); + } else { + memory = this.memory = new MemoryAgent(); + } + memory.configure(scan); + } + } + } + } + } + if(time) { + time.setup(evaluation) + } + if(memory) { + memory.setup(evaluation); + } + } + + unregister() { + if(this.time) this.time.close(); + if(this.memory) this.memory.close(); + } + +} + +export var instance = new SystemDatabase(); diff --git a/src/runtime/errors.ts b/src/runtime/errors.ts new file mode 100644 index 000000000..ad9e2a0d5 --- /dev/null +++ b/src/runtime/errors.ts @@ -0,0 +1,267 @@ +//-------------------------------------------------------------- +// Errors +//-------------------------------------------------------------- + +import {exceptions, Token, EOF} from "chevrotain"; +import * as parser from "./parser"; + +const SPAN_TYPE = "document_comment"; + +//-------------------------------------------------------------- +// EveError +//-------------------------------------------------------------- + +class EveError { + static ID = 0; + + type = "error"; + id: string; + blockId: string; + message: string; + start: number; + stop: number; + context?: any; + spanId: string; + + constructor(blockId, start, stop, message, context?) { + this.blockId = blockId; + this.id = `${blockId}|error|${EveError.ID++}`; + this.start = start; + this.stop = stop; + this.message = message; + this.context = context; + } + + injectSpan(spans, extraInfo) { + spans.push(this.start, this.stop, SPAN_TYPE, this.id); + extraInfo[this.id] = this; + } +} + +//-------------------------------------------------------------- +// Parse error utils +//-------------------------------------------------------------- + +function regexGroup(str, regex, group = 1) { + var matches = []; + var match; + while (match = regex.exec(str)) { + matches.push(match[group]); + } + return matches; +} + +function className(thing) { + var funcNameRegex = /function (.{1,})\(/; + var results = (funcNameRegex).exec((thing).constructor.toString()); + return (results && results.length > 1) ? results[1] : ""; +}; + +function lastTokenWithType(tokens, type) { + let ix = tokens.length - 1; + while(ix >= 0) { + let cur = tokens[ix]; + if(cur instanceof type) { + return cur; + } + ix--; + } +} + + +//-------------------------------------------------------------- +// Parse errors +//-------------------------------------------------------------- + +export function parserErrors(errors: any[], parseInfo: {blockId: string, blockStart: number, spans: any[], extraInfo: any, tokens: Token[]}) { + let {blockId, blockStart, spans, extraInfo} = parseInfo; + let normalized = []; + let errorIx = 1; + + for(let error of errors) { + let {token, context, message, resyncedTokens, name} = error; + + let eveError: EveError; + if(name === "MismatchedTokenException") { + eveError = mismatchedToken(error, parseInfo); + } else if(name === "NotAllInputParsedException") { + eveError = notAllInputParsed(error, parseInfo); + } else { + console.log("UNHANDLED ERROR TYPE", name); + let start = token.startOffset; + let stop = token.startOffset + token.image.length; + eveError = new EveError(blockId, start, stop, message, context); + } + + eveError.injectSpan(spans, extraInfo); + normalized.push(eveError); + } + return normalized; +} + +//-------------------------------------------------------------- +// MismatchedToken parse error +//-------------------------------------------------------------- + +const MismatchRegex = /-->\s*(.*?)\s*<--/gi; + +function mismatchedToken(error, parseInfo) { + const Pairs = { + "CloseString": parser.OpenString, + "CloseBracket": parser.OpenBracket, + "CloseParen": parser.OpenParen, + }; + + let {blockId, blockStart, spans, extraInfo, tokens} = parseInfo; + let {token, context, message, resyncedTokens, name} = error; + + let blockEnd = tokens[tokens.length - 1].endOffset + 1; + + let [expectedType, foundType] = regexGroup(message, MismatchRegex); + + let start, stop; + + if(token instanceof EOF) { + let pair = Pairs[expectedType]; + if(pair) { + token = lastTokenWithType(tokens, pair); + message = messages.unclosedPair(expectedType); + } else { + token = tokens[tokens.length - 1]; + } + stop = blockEnd; + } + + // We didn't find a matching pair, check if we're some other mistmatched bit of syntax. + if(stop === undefined) { + if(expectedType === "Tag") { + if(token.label === "identifier") { + message = messages.actionRawIdentifier(token.image); + } else { + message = messages.actionNonTag(token.image); + } + } + } + + if(start === undefined) start = token.startOffset; + if(stop === undefined) stop = token.startOffset + token.image.length; + + return new EveError(blockId, start, stop, message, context); +} + +//-------------------------------------------------------------- +// NotAllInputParsed parse error +//-------------------------------------------------------------- + +const NotAllInputRegex = /found:\s*([^\s]+)/gi; +const CloseChars = {")": true, "]": true}; + +function notAllInputParsed(error, parseInfo) { + let {blockId, blockStart, spans, extraInfo, tokens} = parseInfo; + let {token, context, message, resyncedTokens, name} = error; + + let blockEnd = tokens[tokens.length - 1].endOffset + 1; + + let [foundChar] = regexGroup(message, NotAllInputRegex); + + let start, stop; + + if(CloseChars[foundChar]) { + message = messages.extraCloseChar(foundChar); + } else { + console.log("WEIRD STUFF AT THE END", context); + } + + if(start === undefined) start = token.startOffset; + if(stop === undefined) stop = token.startOffset + token.image.length; + + return new EveError(blockId, start, stop, message, context); +} + +//-------------------------------------------------------------- +// Build errors +//-------------------------------------------------------------- + +export function unprovidedVariableGroup(block, variables) { + let {id, start: blockStart} = block; + let found; + for(let variable of variables) { + if(!variable.generated) { + found = variable; + break; + } + } + if(!found) { + found = variables[0]; + } + let [start, stop] = parser.nodeToBoundaries(found, blockStart); + return new EveError(id, start, stop, messages.unprovidedVariable(found.name)); +} + +export function invalidLookupAction(block, action) { + let {id, start: blockStart} = block; + let [start, stop] = parser.nodeToBoundaries(action, blockStart); + let missing = []; + if(action.entity === undefined) missing.push("record"); + if(action.attribute === undefined) missing.push("attribute"); + if(action.value === undefined) missing.push("value"); + return new EveError(id, start, stop, messages.invalidLookupAction(missing)); +} + +export function unimplementedExpression(block, expression) { + let {id, start: blockStart} = block; + let [start, stop] = parser.nodeToBoundaries(expression, blockStart); + return new EveError(id, start, stop, messages.unimplementedExpression(expression.op)); +} + +export function incompatabileConstantEquality(block, left, right) { + let {id, start: blockStart} = block; + let [start] = parser.nodeToBoundaries(left, blockStart); + let [_, stop] = parser.nodeToBoundaries(right, blockStart); + return new EveError(id, start, stop, messages.neverEqual(left.value, right.value)); +} + +export function incompatabileVariableToConstantEquality(block, variable, variableValue, constant) { + let {id, start: blockStart} = block; + let [start] = parser.nodeToBoundaries(variable, blockStart); + let [_, stop] = parser.nodeToBoundaries(constant, blockStart); + return new EveError(id, start, stop, messages.variableNeverEqual(variable, variableValue, constant.value)); +} + +export function incompatabileTransitiveEquality(block, variable, value) { + let {id, start: blockStart} = block; + let [start, stop] = parser.nodeToBoundaries(variable, blockStart); + return new EveError(id, start, stop, messages.variableNeverEqual(variable, variable.constant, value)); +} + +//-------------------------------------------------------------- +// Messages +//-------------------------------------------------------------- + +const PairToName = { + "CloseString": "quote", + "CloseBracket": "bracket", + "CloseParen": "paren", + "]": "bracket", + ")": "paren", + "\"": "quote", +} + +export var messages = { + + unclosedPair: (type) => `Looks like a close ${PairToName[type]} is missing`, + + extraCloseChar: (char) => `This close ${PairToName[char]} is missing an open ${PairToName[char]}`, + + unprovidedVariable: (varName) => `Nothing is providing a value for ${varName}`, + + unimplementedExpression: (op) => `There's no definition for the function ${op}`, + + invalidLookupAction: (missing) => `Updating a lookup requires that record, attribute, and value all be provided. Looks like ${missing.join("and")} is missing.`, + + neverEqual: (left, right) => `${left} can never equal ${right}`, + variableNeverEqual: (variable, value, right) => `${variable.name} is equivalent to ${value}, which can't be equal to ${right}`, + + actionNonTag: (found) => `Looks like this should be a tag, try changing the ${found} to a #`, + actionRawIdentifier: (found) => `I can only add/remove tags directly on a record. If you meant to add ${found} as an attribute to the record, try 'my-record.found += ${found}'; if you meant to add the #${found} tag, add #.` +}; diff --git a/src/runtime/eveSource.ts b/src/runtime/eveSource.ts new file mode 100644 index 000000000..6ef370520 --- /dev/null +++ b/src/runtime/eveSource.ts @@ -0,0 +1,176 @@ +export let workspaces:{[name:string]: string} = {}; + +//--------------------------------------------------------- +// Public +//--------------------------------------------------------- + +export function add(name: string, directory: string) { + // If we're running on a windows server, normalize slashes + if(typeof window === undefined) { + if(process.platform.search(/^win/)) { + directory = directory.replace("\\", "/"); + } + } + + if(directory[directory.length - 1] !== "/") directory += "/"; + + if(workspaces[name] && workspaces[name] !== directory) + throw new Error(`Unable to link pre-existing workspace '$[name}' to '${directory}' (currently '${workspaces[name]}')`); + + workspaces[name] = directory; +} + +/** Given an explicit workspace, return the contents of the file. */ +export function get(file:string, workspace = "eve"):string|undefined { + if(!workspaces[workspace]) { + console.error(`Unable to get '${file}' from unregistered workspace '${workspace}'`); + return; + } + + return fetchFile(file, workspace); +} + +/** Using the inferred workspace from the file path, return the contents of the file. */ +export function find(file:string):string|undefined { + let workspace = getWorkspaceFromPath(file); + if(!workspace) return; + + return get(file, workspace); +} + +/** Given an explicit workspace, update the contents of the file. */ +export function set(file:string, content:string, workspace = "eve") { + if(!workspaces[workspace]) { + console.error(`Unable to set '${file}' from unregistered workspace '${workspace}'`); + return; + } + + saveFile(file, content, workspace); +} + +/** Using the inferred workspace from the file path, update the contents of the file. */ +export function save(file:string, content:string) { + let workspace = getWorkspaceFromPath(file); + if(!workspace) return; + + return set(file, content, workspace); +} + +//--------------------------------------------------------- +// Utilities +//--------------------------------------------------------- + + +export function getWorkspaceFromPath(file:string):string|undefined { + var parts = file.split("/"); + var basename = parts.pop(); + var workspace = parts[1]; + if(!basename || !workspace) return; + if(!workspaces[workspace]) { + console.error(`Unable to get '${file}' from unregistered workspace '${workspace}'`); + } + + return workspace; +} + +export function getRelativePath(file:string, workspace:string):string|undefined { + let directory = workspaces[workspace]; + if(!directory) { + console.error(`Unable to get relative path for '${file}' in unregistered workspace '${workspace}'`); + return; + } + + if(file.indexOf("./") === 0) { + file = file.slice(2); + } + + if(file.indexOf(directory) === 0) { + file = file.slice(directory.length); + } + return "/" + workspace + "/" + file; +} + +export function getAbsolutePath(file:string, workspace:string) { + let directory = workspaces[workspace]; + if(file.indexOf(directory) === 0) return file; + + if(file.indexOf("/" + workspace + "/") === 0) file = file.slice(workspace.length + 2); + return directory + file; +} + +//--------------------------------------------------------- +// Server/Client Implementations +//--------------------------------------------------------- + +var saveFile = function(file:string, content:string, workspace:string) { + let cache = global["_workspaceCache"][workspace]; + cache = global["_workspaceCache"][workspace] = {}; + file = getRelativePath(file, workspace); + cache[file] = content; +} + +// If we're running on the client, we use the global _workspaceCache, created in the build phase or served by the server. +var fetchFile = function(file:string, workspace:string):string|undefined { + let cache = global["_workspaceCache"][workspace]; + file = getRelativePath(file, workspace); + return cache && cache[file]; +} + +var fetchWorkspace = function(workspace:string) { + return global["_workspaceCache"][workspace]; +} + +// If we're running on the server, we use the actual file-system. +if(typeof window === "undefined") { + let glob = require("glob"); + let fs = require("fs"); + let path = require("path"); + let mkdirp = require("mkdirp"); + + saveFile = function(file:string, content:string, workspace:string) { + try { + let filepath = getAbsolutePath(file, workspace); + let dirname = path.dirname(filepath); + mkdirp.sync(dirname); + fs.writeFileSync(filepath, content); + } catch(err) { + console.warn(`Unable to save file '${file}' in '${workspace}' containing:\n${content}`); + } + } + + fetchFile = function(file:string, workspace:string):string|undefined { + try { + let filepath = getAbsolutePath(file, workspace); + return fs.readFileSync(filepath).toString(); + } catch(err) { + console.warn(`Unable to find file '${file}' in '${workspace}'`); + } + } + + fetchWorkspace = function(workspace:string) { + let directory = workspaces[workspace]; + let files = {}; + for(let file of glob.sync(directory + "/**/*.eve", {ignore: directory + "**/node_modules/**/*.eve"})) { + let rel = path.relative(directory, file); + files["/" + workspace + "/" + rel] = fs.readFileSync(file).toString(); + } + + return files; + } +} + +export function pack() { + let packaged = {}; + for(let workspace in workspaces) { + packaged[workspace] = fetchWorkspace(workspace); + } + + return `var _workspaceCache = ${JSON.stringify(packaged, null, 2)};\n`; +} + +// If we're running on the client, load the server's workspaces from the cache it passes us. +if(global["_workspaceCache"]) { + for(let workspace in global["_workspaceCache"]) { + add(workspace, workspace); + } +} diff --git a/src/runtime/id.ts b/src/runtime/id.ts new file mode 100644 index 000000000..af7ba3904 --- /dev/null +++ b/src/runtime/id.ts @@ -0,0 +1,63 @@ +class IdStore { + currentId = 0; + partsToId: Object = Object.create(null); + idToParts: Object = Object.create(null); + + _makeStringId() { + return `⦑${this.currentId++}⦒`; + } + + _make(origKey: string, parts: any[]) { + let ix = 0; + let changed = false; + for(let part of parts) { + let found = this.idToParts[part] + if(found !== undefined) { + parts[ix] = found; + changed = true; + } + ix++; + } + let updatedKey = origKey; + if(changed) { + updatedKey = `⦑${parts.join("⦒")}`; + } + let id = this._makeStringId(); + let loadedValue = this.partsToId[updatedKey]; + if(loadedValue) { + this.partsToId[origKey] = loadedValue; + this.idToParts[loadedValue] = updatedKey; + } else { + this.partsToId[origKey] = id; + this.idToParts[id] = updatedKey; + } + return id; + } + + isId(id: any) { + return id.substring && id[0] === "⦑"; + } + + load(id: string) { + let found = this.partsToId[id]; + if(found) return found; + let neue = this._makeStringId(); + this.partsToId[id] = neue; + this.idToParts[neue] = id; + return neue; + } + + get(parts: any[]) { + let key = `⦑${parts.join("⦒")}`; + let id = this.partsToId[key]; + if(id) return id; + return this._make(key, parts); + } + + parts(id: string) { + return this.idToParts[id]; + } +} + +export var ids = new IdStore(); + diff --git a/src/runtime/indexes.ts b/src/runtime/indexes.ts new file mode 100644 index 000000000..85c8888d8 --- /dev/null +++ b/src/runtime/indexes.ts @@ -0,0 +1,295 @@ +//--------------------------------------------------------------------- +// Indexes +//--------------------------------------------------------------------- + +import {ids} from "./id"; + + +//--------------------------------------------------------------------- +// MultiIndex +//--------------------------------------------------------------------- + +export class MultiIndex { + indexes: {[name: string]: TripleIndex}; + scopes: string[]; + constructor() { + this.indexes = {}; + this.scopes = []; + } + + register(name, index = new TripleIndex(0)) { + this.indexes[name] = index; + if(this.scopes.indexOf(name) === -1) { + this.scopes.push(name); + } + return index; + } + + unregister(name) { + this.indexes[name] = undefined; + this.scopes.splice(this.scopes.indexOf(name), 1); + } + + getIndex(name) { + let index = this.indexes[name]; + if(!index) return this.register(name); + return index; + } + + dangerousMergeLookup(e,a?,v?,node?) { + let results = []; + let indexes = this.indexes; + for(let scope of this.scopes) { + let index = indexes[scope]; + if(index === undefined) continue; + let found = index.lookup(e,a,v,node); + if(found) { + let foundIndex = found.index; + for(let key of Object.keys(foundIndex)) { + results.push(foundIndex[key].value); + } + } + } + return results; + } + + contains(scopes, e, a?, v?, node?) { + let indexes = this.indexes; + for(let scope of scopes) { + let index = indexes[scope]; + if(index === undefined) continue; + if(index.lookup(e,a,v,node) !== undefined) return true; + } + return; + } + + store(scopes, e, a?, v?, node?) { + let indexes = this.indexes; + for(let scope of scopes) { + let index = indexes[scope]; + if(index === undefined) { + index = this.register(scope); + } + index.store(e,a,v,node) + } + } + + unstore(scopes, e, a?, v?, node?) { + let indexes = this.indexes; + for(let scope of scopes) { + let index = indexes[scope]; + if(index === undefined) continue; + index.unstore(e,a,v,node) + } + } +} + +export class TripleIndex { + cardinalityEstimate: number; + version: number; + eavIndex: IndexLevel; + aveIndex: IndexLevel; + neavIndex: IndexLevel; + constructor(version: number, eavIndex?: IndexLevel, aveIndex?: IndexLevel, neavIndex?: IndexLevel) { + this.cardinalityEstimate = 0; + this.version = version; + this.eavIndex = eavIndex !== undefined ? eavIndex : new IndexLevel(0, "eavRoot"); + this.aveIndex = aveIndex !== undefined ? aveIndex : new IndexLevel(0, "aveRoot"); + this.neavIndex = neavIndex !== undefined ? neavIndex : new IndexLevel(0, "neavRoot"); + } + + store(e,a,v,node = "user") { + this.cardinalityEstimate++; + this.eavIndex = this.eavIndex.store(this.version, e,a,v,node); + this.aveIndex = this.aveIndex.store(this.version, a,v,e,node); + this.neavIndex = this.neavIndex.store(this.version, node,e,a,v); + } + + unstore(e,a,v,node?) { + let changed = this.eavIndex.unstore(this.version,e,a,v,node); + if(changed) { + this.cardinalityEstimate--; + this.eavIndex = changed; + this.aveIndex = this.aveIndex.unstore(this.version,a,v,e,node); + this.neavIndex = this.neavIndex.unstore(this.version,node,e,a,v); + } + } + + asValues(e, a?, v?, node?, recursive = false, singleAttributes = false) { + let level = this.eavIndex.lookup(e,a,v,node); + if(level) { + let index = level.index; + let values = []; + for(let key of Object.keys(index)) { + let value = index[key].value; + if(!recursive || this.eavIndex.lookup(value) === undefined) { + values.push(value); + } else { + values.push(this.asObject(value, recursive)); + } + if(singleAttributes) return values[0]; + } + return values; + } + return; + } + + asObject(e, recursive = false, singleAttributes = false) : any { + let obj = {}; + let attributes = this.asValues(e); + if(attributes) { + for(let attribute of attributes) { + obj[attribute] = this.asValues(e, attribute, undefined, undefined, recursive, singleAttributes); + } + } + return obj; + } + + toTriples(withNode?, startIndex?) { + let triples = []; + let eavIndex = startIndex || this.eavIndex.index; + let current = []; + for(let eKey of Object.keys(eavIndex)) { + let eInfo = eavIndex[eKey] as IndexLevel; + current[0] = eInfo.value; + let aIndex = eInfo.index + for(let aKey of Object.keys(aIndex)) { + let aInfo = aIndex[aKey] as IndexLevel; + current[1] = aInfo.value; + let vIndex = aInfo.index; + for(let vKey of Object.keys(vIndex)) { + let vInfo = vIndex[vKey] as IndexLevel; + if(vInfo.value !== undefined) { + current[2] = vInfo.value; + } else { + current[2] = vInfo; + } + if(withNode) { + let nIndex = vInfo.index; + for(let nKey of Object.keys(nIndex)) { + let nInfo = nIndex[nKey]; + current[3] = nInfo; + triples.push(current.slice()); + } + } else { + triples.push(current.slice()); + } + } + } + } + return triples; + } + + // find an eav in the indexes + lookup(e,a?,v?,node?) { + // let start = perf.time(); + let result = this.eavIndex.lookup(e,a,v,node) + // perf.lookup(start); + return result; + } + + // find an ave in the indexes + alookup(a?,v?,e?,node?) { + // let start = perf.time(); + let result = this.aveIndex.lookup(a,v,e,node) + // perf.lookup(start); + return result; + } + + nodeLookup(node?,e?,a?,v?) { + let result = this.neavIndex.lookup(node,e,a,v); + return result; + } + + nextVersion() { + return new TripleIndex(this.version + 1, this.eavIndex, this.aveIndex); + } +} + +class IndexLevel { + version: number; + value: any; + cardinality: number; + index: {[key: string]: IndexLevel | string}; + constructor(version: number, value: any) { + this.version = version; + this.value = value; + this.cardinality = 0; + this.index = {}; + } + + store(version, a,b?,c?,d?,e?,f?,g?,h?,i?,j?) { + let child = this.index[a]; + let newChild = a; + if(child === undefined && b !== undefined) { + newChild = new IndexLevel(version, a); + newChild.store(version, b,c,d,e,f,g,h,i,j); + } else if(b !== undefined) { + newChild = (child as IndexLevel).store(version, b,c,d,e,f,g,h,i,j); + } + let updated : IndexLevel = this; + if(newChild.version > this.version) { + // updated = this.clone(version) + } + if(child === undefined) { updated.cardinality++; } + updated.index[a] = newChild; + return updated; + } + + unstore(version, a,b?,c?,d?,e?,f?,g?,h?,i?,j?) { + let child = this.index[a]; + if(child === undefined) return; + + let updated: IndexLevel = this; + + if(child instanceof IndexLevel) { + let updatedChild = child.unstore(version, b,c,d,e,f,g,h,i,j); + if(updatedChild === undefined) { + // updated = this.clone(version); + delete updated.index[a]; + updated.cardinality--; + } else { + // updated = this.clone(version); + updated.index[a] = updatedChild; + } + } else { + // updated = this.clone(version); + delete updated.index[a]; + updated.cardinality--; + } + if(updated.cardinality <= 0) { + return; + } + return updated; + } + + toValues() { + let values = []; + for(let key of Object.keys(this.index)) { + let value: any = this.index[key]; + values.push(value.value || value); + } + return values; + } + + lookup(a,b?,c?,d?,e?,f?,g?,h?,i?,j?) { + let child = this.index[a]; + if(child === undefined) return; + if(b !== undefined && child instanceof IndexLevel) { + return child.lookup(b,c,d,e,f,g,h,i,j); + } + return child; + } + + clone(version) { + let next = new IndexLevel(version, this.value); + next.cardinality = this.cardinality; + let index = next.index; + let originalIndex = this.index; + let keys = Object.keys(originalIndex); + for(let key of keys) { + index[key] = originalIndex[key]; + } + return next; + } +} diff --git a/src/runtime/join.ts b/src/runtime/join.ts new file mode 100644 index 000000000..3528a4fb7 --- /dev/null +++ b/src/runtime/join.ts @@ -0,0 +1,892 @@ +//--------------------------------------------------------------------- +// Generic join in Typescript over triples (EAVs) +//--------------------------------------------------------------------- + +let perf = global["perf"]; + +import {MultiIndex, TripleIndex} from "./indexes"; +import {Block, BlockStratum, scansToVars} from "./block"; +import {Changes} from "./changes"; +import {Aggregate} from "./providers/aggregate"; +import {ids} from "./id"; +import * as providers from "./providers/index"; + +//--------------------------------------------------------------------- +// UUID +//--------------------------------------------------------------------- + +let _idArray = []; +function makeUUID(idprefix, projection) { + _idArray[0] = idprefix; + let ix = 1; + for(let proj of projection) { + _idArray[ix] = proj; + ix++; + } + _idArray.length = ix; + return ids.get(_idArray); +} + +//--------------------------------------------------------------------- +// Variable +//--------------------------------------------------------------------- + +// We'll use Variable to represent relational variables in our "queries." +// These will be values used in both scans and constraints +export class Variable { + id: string; + constant?: any; + constructor(id) { + this.id = id; + } +} + +export function isVariable(thing) { + return thing instanceof Variable; +} + +//--------------------------------------------------------------------- +// Proposal +//--------------------------------------------------------------------- + +// In generic join, each scan/constraint proposes a variable to solve for +// and what cardinality that variable would have if you choose its proposal. +export interface Proposal { + providing: Variable | Variable[], + cardinality: number, + // optional bits of information for someone trying to resolve a proposal + index?:any, // the index to use when resolving + indexType?: any, // type of the index used to resolve +} + +// Constraints/scans/etc are providers of proposals +export interface ProposalProvider { + vars: Variable[], + // Given a prefix of solved variables, return a proposal for + // solving a new variable + propose(index: MultiIndex, prefix: any[]) : Proposal, + // Take a proposal and resolve it into the actual values being + // proposed + resolveProposal(index: MultiIndex, proposal: Proposal, prefix: any[]) : any[], + // Check if a prefix of solved variables is a valid potential solution + // for this provider. SolvingFor is used to ignore accept calls that + // aren't related to variables the provider is solving for. + accept(index: MultiIndex, prefix: any[], solvingFor: Variable, force?: boolean, prejoin?: boolean): boolean +} + +//--------------------------------------------------------------------- +// Prefix functions +//--------------------------------------------------------------------- + +// Turn a "register" (either an arg or return) into a value based +// on a prefix of variables +export function toValue(register, prefix) { + if(isVariable(register)) { + return prefix[register.id]; + } + return register; +} + +// Resolve an array of registers based on a prefix of variables +export function resolve(toResolve, prefix, resolved = []) { + let ix = 0; + for(let register of toResolve) { + resolved[ix] = toValue(register, prefix); + ix++; + } + return resolved; +} + +// Check if this entire array of registers has values (all variables have been +// filled in by the prefix.) +function fullyResolved(toCheck, prefix) { + for(let register of toCheck) { + if(register === undefined) continue; + if(toValue(register, prefix) === undefined) return false; + } + return true; +} + +//--------------------------------------------------------------------- +// Scan +//--------------------------------------------------------------------- + +// Scans are structures that represent looking up eavs in the indexes. +// You specify a triple that they should look for which can have variables +// or constant values for e, a, or v that we'll attempt to solve for. +export class Scan { + id: string; + // array representation of the eav + eav: any[]; + // a "bitmap" for what variables this scan is solving for + // we use index as the key and the variable as the value + vars: Variable[]; + // blown out eav for convenience + e: any; + a: any; + v: any; + node: any; + proposalObject: Proposal; + resolved: any[]; + scopes: string[]; + + constructor(id: string, e,a,v,node?,scopes?) { + this.id = id; + this.resolved = []; + this.eav = [e,a,v,node]; + this.e = e; + this.a = a; + this.v = v; + this.node = node; + this.proposalObject = {providing: null, index: [], cardinality: 0}; + this.scopes = scopes || ["session"]; + + // check if any of the supplied params are variables and store them + this.vars = []; + for(let register of this.eav) { + if(isVariable(register)) { + this.vars[register.id] = register; + } + } + } + + // Return an array of the current values for all the registers + resolve(prefix) { + let resolved = this.resolved; + resolved[0] = toValue(this.e, prefix); + resolved[1] = toValue(this.a, prefix); + resolved[2] = toValue(this.v, prefix); + resolved[3] = toValue(this.node, prefix); + return resolved; + } + + + _fullScanLookup(index, solving, results, resolved, solvingIx, ix, maxDepth) { + if(index === undefined) return; + if(ix === maxDepth) { + return results.push(solving.slice()); + } + let value = resolved[ix]; + if(value === undefined) { + let curIndex = index.index; + for(let key of Object.keys(curIndex)) { + let v = curIndex[key]; + solving[solvingIx] = v.value !== undefined ? v.value : v; + this._fullScanLookup(v, solving, results, resolved, solvingIx + 1, ix + 1, maxDepth); + } + } else { + this._fullScanLookup(index.index[value], solving, results, resolved, solvingIx, ix + 1, maxDepth); + } + } + + fullScan(index, resolved, results) { + let [e,a,v,node] = resolved; + let solving = []; + let solveNode = this.node !== undefined; + let depth = solveNode ? 4 : 3; + if(a !== undefined) { + this._fullScanLookup(index.aveIndex, solving, results, [a,v,e,node], 0, 0, depth); + } else { + this._fullScanLookup(index.eavIndex, solving, results, resolved, 0, 0, depth); + } + return results; + } + + setProposal(index, toProvide, scopeIx) { + let proposal = this.proposalObject; + if(index) { + proposal.providing = toProvide; + proposal.index[scopeIx] = index.index; + proposal.cardinality += index.cardinality; + return true; + } + proposal.index[scopeIx] = undefined; + return false; + } + + toLookupType(resolved) { + let [e,a,v,node] = resolved; + let foo = []; + if(e === undefined) foo[0] = "*" + else foo[0] = "e"; + if(a === undefined) foo[1] = "*" + else foo[1] = "a"; + if(v === undefined) foo[2] = "*" + else foo[2] = "v"; + if(node === undefined) foo[3] = "*" + else foo[3] = "n"; + return foo.join(""); + } + + // Given a resolved array of values for all the registers, find out which variable we could + // make a proposal for, what index we'd use to get the values for it, and what the cardinality + // of the proposal is. + getProposal(multiIndex, resolved) { + let [e,a,v,node] = resolved; + const lookupType = this.toLookupType(resolved); + let proposal = this.proposalObject; + proposal.providing = undefined; + proposal.indexType = undefined; + proposal.cardinality = 0; + let scopeIx = 0; + for(let scope of this.scopes) { + let curIndex = multiIndex.getIndex(scope); + switch(lookupType) { + case "e***": + this.setProposal(curIndex.eavIndex.lookup(e), this.a, scopeIx); + break; + case "ea**": + this.setProposal(curIndex.eavIndex.lookup(e,a), this.v, scopeIx); + break; + case "eav*": + this.setProposal(curIndex.eavIndex.lookup(e,a,v), this.node, scopeIx); + break; + case "*a**": + this.setProposal(curIndex.aveIndex.lookup(a), this.v, scopeIx); + break; + case "*av*": + this.setProposal(curIndex.aveIndex.lookup(a,v), this.e, scopeIx); + break; + case "***n": + this.setProposal(curIndex.neavIndex.lookup(node), this.e, scopeIx); + break; + case "e**n": + this.setProposal(curIndex.neavIndex.lookup(node,e), this.a, scopeIx); + break; + case "ea*n": + this.setProposal(curIndex.neavIndex.lookup(node,e,a), this.v, scopeIx); + break; + default: + if(proposal.providing === undefined) { + let providing = proposal.providing = []; + if(e === undefined) providing.push(this.e); + if(a === undefined) providing.push(this.a); + if(v === undefined) providing.push(this.v); + if(node === undefined && this.node !== undefined) providing.push(this.node); + } + // full scan + proposal.index[scopeIx] = curIndex; + proposal.cardinality += curIndex.cardinalityEstimate; + proposal.indexType = "fullScan"; + break; + } + scopeIx++; + } + return proposal; + } + + // Return a proposal or nothing based on the currently solved prefix of variables. + propose(tripleIndex, prefix) : Proposal | undefined { + let resolved = this.resolve(prefix); + let [e,a,v,node] = resolved; + // if this scan is fully resolved, then there's no variable for us to propose + if(e !== undefined && a !== undefined && v !== undefined && (node !== undefined || this.node === undefined)) { + return; + } + return this.getProposal(tripleIndex, resolved); + } + + // Given a proposal, get the values for that proposal. There are two proposal types + // for scans purely because of the way we wrote our indexes. Because JS will turn all + // object keys into strings, we have to check if we're looking for real values. If we aren't + // we can just return the string keys, otherwise we have to take the extra step of getting + // all the actual values. If we didn't do this, we'd end up with strings instead of numbers + // for things like someone's age. + resolveProposal(proposal, prefix) { + let values = []; + let indexes = proposal.index; + if(indexes === undefined || indexes.length == 0) { + return values; + } + if(proposal.indexType !== "fullScan") { + let ix = 0; + for(let index of indexes) { + if(index === undefined) continue; + let keys = Object.keys(index); + let node = this.node; + for(let key of keys) { + let value = index[key]; + values[ix] = value.value === undefined ? value : value.value; + ix++; + } + } + } else { + let resolved = this.resolve(prefix); + for(let index of indexes) { + this.fullScan(index, resolved, values); + } + } + return values; + } + + // Given a prefix and a variable that we're solving for, we check if we agree with the + // current set of values. If this scan is completely resolved, we check for the presence + // of the value given all the filled variables. If not, we check if there's an index that + // could provide us the rest of it. + accept(multiIndex: MultiIndex, prefix, solvingFor, force?) { + // we only need to check if we're solving for a variable that is actually part of our + // scan + if(!force && !this.vars[solvingFor.id]) return true; + let resolved = this.resolve(prefix); + let [e,a,v,node] = resolved; + // check if we're fully resolved and if so lookup to see if we accept + if(e !== undefined && a !== undefined && v !== undefined) { + if(this.node !== undefined) { + //multidb + return multiIndex.contains(this.scopes,e,a,v,node) !== undefined; + } + return multiIndex.contains(this.scopes,e,a,v) !== undefined; + } + // we can check if we get a proposal with a cardinality to determine if we can + // accept this prefix. If we don't it means there are no values for the remaining + // vars in the indexes. + let proposal = this.getProposal(multiIndex, resolved); + return proposal && proposal.cardinality > 0; + } +} + +//--------------------------------------------------------------------- +// Constraint +//--------------------------------------------------------------------- + +// Like Scan, Constraint is a structure that represents a constraint or function +// in our "queries". Constraints have both an array of args and an array of returns, +// either of which can contain variables or constants. +export abstract class Constraint { + id: string; + args: any[]; + returns: any[]; + proposalObject: Proposal; + resolvedArgs: any[]; + resolvedReturns: any[]; + resolved: {args: any[], returns: any[]}; + // like in scan this is a "bitmap" of the variables this constraint + // deals with. This includes vars from both args and returns. + vars: Variable[]; + + constructor(id: string, args: any[], returns: any[]) { + this.id = id; + this.args = args; + this.returns = returns; + this.proposalObject = {providing: null, cardinality: 0} + this.resolvedArgs = []; + this.resolvedReturns = []; + this.resolved = {args: null, returns: null}; + this.vars = []; + // capture our variables + for(let register of this.args) { + if(isVariable(register)) { + this.vars[register.id] = register; + } + } + for(let register of this.returns) { + if(isVariable(register)) { + this.vars[register.id] = register; + } + } + } + + resolve(prefix) { + let resolved = this.resolved; + resolved.args = resolve(this.args, prefix, this.resolvedArgs); + resolved.returns = resolve(this.returns, prefix, this.resolvedReturns); + return resolved; + } + + // In the case of a constraint, it only makes sense to propose an extension + // to the prefix if either our args are fully resolved, but our returns aren't. + // If that's the case, then our proposal will be to fill in our returns. + propose(tripleIndex, prefix) { + // if either our inputs aren't resolved or our returns are all filled + // in, then we don't have anything to propose + if(!fullyResolved(this.args, prefix) + || fullyResolved(this.returns, prefix)) return; + + // find out which of our returns we could propose a value for + let proposed; + for(let ret of this.returns) { + if(toValue(ret, prefix) === undefined) { + proposed = ret; + break; + } + } + + // Each implementation of a constraint has to provide what its potential + // cardinality will be. Raw constraints like >, for example, will never + // make a proposal, while something like + might return cardinality 1, and + // split some approximation. + return this.getProposal(tripleIndex, proposed, prefix); + } + + // Constraints accept a prefix if either we're solving for something unrelated, + // if their args aren't fully resolved yet (we can't compute yet!) or if their + // returns aren't fully resolved (what would we check against?) + accept(tripleIndex, prefix, solvingFor, force?) { + if(!force && + !this.vars[solvingFor.id] + || !fullyResolved(this.args, prefix) + || !fullyResolved(this.returns, prefix)) return true; + + // otherwise we leave it to the constraint to implement an acceptance test + return this.test(prefix); + } + + // Given a variable to solve for and a prefix of solved variables, return + // a proposal for that variable + abstract getProposal(tripleIndex: TripleIndex, proposed: Variable, prefix: any) : Proposal | undefined; + + // Resolve a proposal you provided into the actual values for a variable + abstract resolveProposal(proposal: Proposal, prefix: any[]) : any[]; + + // Test if a prefix adheres to the constraint being implemented + abstract test(prefix: any) : boolean; +} + +//--------------------------------------------------------------------- +// Some constraint implementations +//--------------------------------------------------------------------- + +class GenerateId extends Constraint { + resolveProposal(proposal, prefix) { + let {args} = this.resolve(prefix); + return [makeUUID(this.id, args)]; + } + + test(prefix) { + let {args, returns} = this.resolve(prefix); + return returns[0] === makeUUID(this.id, args); + } + + getProposal(tripleIndex, proposed, prefix) { + let proposal = this.proposalObject; + proposal.providing = proposed; + proposal.cardinality = 1; + return proposal; + } +} + +providers.provide("generateId", GenerateId); + +//--------------------------------------------------------------------- +// NotScan +//--------------------------------------------------------------------- + +export class NotScan { + id: string; + strata: BlockStratum[]; + vars: Variable[]; + args: Variable[]; + internalVars: Variable[]; + resolved: any[]; + + constructor(id: string, args: Variable[], strata: BlockStratum[]) { + this.id = id; + this.strata = strata; + this.resolved = []; + let blockVars = []; + scansToVars(strata, blockVars); + this.vars = args; + this.args = args; + this.internalVars = blockVars; + } + + resolve(prefix) { + return resolve(this.args, prefix, this.resolved); + } + + propose() { return; } + resolveProposal() { throw new Error("Resolving a not proposal"); } + + accept(multiIndex: MultiIndex, prefix, solvingFor, force?, prejoin?) { + // if we're in the prejoin phase and this not has no args, then we need + // to evaluate the not to see if we should run. If we didn't do this, arg-less + // nots won't get evaluated during Generic Join since we're never solving for a + // variable that this scan cares about. + if((!prejoin || this.args.length) + // if we aren't forcing and not solving for the current variable, then we just accept + // as it is + && (!force && !this.internalVars[solvingFor.id] && this.internalVars.length) + // we also blind accept if we have args that haven't been filled in yet, as we don't + // have the dependencies necessary to make a decision + || !fullyResolved(this.args, prefix)) return true; + let resolved = this.resolve(prefix); + let notPrefix = []; + let ix = 0; + for(let arg of this.args) { + notPrefix[arg.id] = resolved[ix]; + ix++; + } + // console.log("checking not", notPrefix, this.internalVars); + let results = [notPrefix]; + if(this.strata.length === 1) { + results = this.strata[0].execute(multiIndex, results, {single: true}); + } else { + for(let stratum of this.strata) { + results = stratum.execute(multiIndex, results); + if(results.length === 0) break; + } + } + // console.log("checked not!", results.length); + return !results.length; + } + +} + +//--------------------------------------------------------------------- +// IfScan +//--------------------------------------------------------------------- + +export class IfBranch { + id: string; + outputs: any[]; + strata: BlockStratum[]; + prefix: any[]; + variables: any[]; + exclusive: boolean; + constantReturn: boolean; + constructor(id: string, strata: BlockStratum[], outputs: any[], exclusive?: boolean) { + this.id = id; + this.strata = strata; + this.outputs = outputs; + this.exclusive = exclusive; + this.variables = []; + this.constantReturn = true; + scansToVars(strata, this.variables); + for(let output of outputs) { + if(isVariable(output)) { + this.constantReturn = false; + this.variables[output.id] = output; + } + } + this.prefix = []; + } + resolve(prefix) { + return resolve(this.variables, prefix, this.prefix); + } + + execute(multiIndex: MultiIndex, result) { + if(this.constantReturn && this.strata.length === 1) { + result = this.strata[0].execute(multiIndex, result, {single: true}); + } else { + for(let stratum of this.strata) { + result = stratum.execute(multiIndex, result); + if(result.length === 0) break; + } + } + return result; + } +} + +export class IfScan implements ProposalProvider { + id: string; + branches: IfBranch[]; + vars: Variable[]; + args: Variable[]; + outputs: Variable[]; + internalVars: Variable[]; + resolved: any[]; + resolvedOutputs: any[]; + exclusive: boolean; + hasAggregate: boolean; + hasResolvedOutputs: boolean; + proposalObject: Proposal; + + constructor(id: string, args: Variable[], outputs: Variable[], branches: IfBranch[], hasAggregate = false) { + this.id = id; + this.branches = branches; + this.outputs = outputs; + this.hasAggregate = hasAggregate; + this.resolved = []; + this.resolvedOutputs = []; + this.hasResolvedOutputs = false; + let blockVars = []; + this.vars = args.slice(); + for(let branch of branches) { + if(branch.exclusive) this.exclusive = true; + } + for(let output of outputs) { + if(output !== undefined && isVariable(output)) { + this.vars[output.id] = output; + blockVars[output.id] = output; + } + } + for(let arg of args) { + if(isVariable(arg)) { + blockVars[arg.id] = arg; + } + } + this.args = args; + this.internalVars = blockVars; + this.proposalObject = {providing: null, index: null, cardinality: 0}; + } + + resolve(prefix) { + return resolve(this.args, prefix, this.resolved); + } + + resolveOutputs(prefix) { + this.hasResolvedOutputs = false; + let resolved = resolve(this.outputs, prefix, this.resolvedOutputs); + for(let item of resolved) { + if(item !== undefined) { + this.hasResolvedOutputs = true; + break; + } + } + return resolved; + } + + checkOutputs(resolved, row) { + if(!this.hasResolvedOutputs) return true; + let ix = 0; + for(let item of resolved) { + if(item !== undefined && item !== row[ix]) { + return false; + } + } + return true; + } + + getProposal(multiIndex: MultiIndex, proposed, proposedIx, prefix) { + let proposalValues = []; + let cardinality = 0; + let resolvedOutputs = this.resolveOutputs(prefix); + let projection = {}; + for(let branch of this.branches) { + let branchPrefix = branch.resolve(prefix); + let result = [branchPrefix]; + result = branch.execute(multiIndex, result); + if(result.length) { + for(let row of result) { + let outputRow = []; + for(let output of branch.outputs) { + let value = toValue(output, row); + outputRow.push(value); + } + if(!this.checkOutputs(resolvedOutputs, outputRow)) { + continue; + } + let key = outputRow.join("|"); + if(projection[key] === undefined) { + projection[key] = true; + proposalValues.push(outputRow); + cardinality++; + } + } + if(this.exclusive) break; + } + } + let proposal = this.proposalObject; + proposal.providing = this.outputs; + proposal.index = proposalValues; + proposal.cardinality = cardinality; + return proposal; + } + + propose(multiIndex: MultiIndex, prefix) { + // if either our inputs aren't resolved or our outputs are all filled + // in, then we don't have anything to propose + if(!fullyResolved(this.args, prefix) + || fullyResolved(this.outputs, prefix)) return; + + // find out which of our outputs we could propose a value for + let proposed; + let proposedIx = 0; + for(let ret of this.outputs) { + if(toValue(ret, prefix) === undefined) { + proposed = ret; + break; + } + proposedIx++; + } + + return this.getProposal(multiIndex, proposed, proposedIx, prefix); + } + + resolveProposal(proposal, prefix) { + return proposal.index; + } + + accept(multiIndex: MultiIndex, prefix, solvingFor, force?) { + if(!force && !this.internalVars[solvingFor.id] || !fullyResolved(this.args, prefix)) return true; + for(let branch of this.branches) { + for(let stratum of branch.strata) { + let result = preJoinAccept(multiIndex, stratum.scans, stratum.vars, prefix); + if(result.accepted) { + return true; + } + } + } + return false; + } + +} + +//--------------------------------------------------------------------- +// Generic Join +//--------------------------------------------------------------------- + +// Generic join functions by going through proposals for each variable being +// solved for. This happens in "rounds" where we solve an individual variable +// at a time. Unlike most join algorithms, no ordering is fixed here. Instead, +// proposals are issued and the best, based on lowest cardinality, is selected +// and used as the current variable to solve for. It's important to note that this +// happens based on the values of the currently solved "prefix" - a partially filled +// row of values - which means that generic join chooses an order for each unique +// set of values it comes into contact with. This implementation uses recursion to +// do subsequent rounds for a given prefix and only allocates a row when a fully +// validated result has been found. +// +// A join round takes a set of providers, the current prefix, how many rounds are remaining, +// and an array to hold accepted rows. +function joinRound(multiIndex: MultiIndex, providers: ProposalProvider[], prefix: any[], rounds: number, rows: any[], options: any) { + let {solverInfo} = options; + // To start out we need to find the best proposal given the providers we have. We'll + // start our bestProposal out at some horrible cardinality + let bestProposal: Proposal = {providing: undefined, cardinality: Infinity}; + let bestProvider, bestProviderIx; + let ix = 0; + // Walk through the providers and ask for proposals + for(let provider of providers) { + let proposed = provider.propose(multiIndex, prefix); + // if we've found a lower cardinality, we want to keep track of that provider + if(proposed !== undefined && proposed.cardinality < bestProposal.cardinality) { + bestProposal = proposed; + bestProvider = provider; + bestProviderIx = ix; + } + ix++; + } + + // console.log("Best provider", rounds, bestProvider, bestProposal); + // if we never found a provider that means we have no more valid solutions + // and we have nothing more to do + if(bestProvider === undefined || bestProposal.cardinality === 0) { + if(bestProviderIx !== undefined) solverInfo[bestProviderIx]++; + return; + } + + // Otherwise, we ask the provider to resolve their proposal into values that + // we then need to see if the other providers accept. + let values = bestProvider.resolveProposal(bestProposal, prefix); + let providing:any = bestProposal.providing; + let providingOne = providing.constructor !== Array; + if(providingOne) { + providing = [providing]; + } + let providingLength = providing.length; + for(let value of values) { + // Set the current value in our prefix of solved variables + let providingIx = 0; + for(let currentProvide of providing) { + if(providingOne) { + prefix[currentProvide.id] = value; + } else { + prefix[currentProvide.id] = value[providingIx]; + } + providingIx++; + } + // Unless someone tells us otherwise, we'll assume that we can accept + // this proposal and continue solving + let accepted = true; + let providerIx = 0; + for(let provider of providers) { + // we don't need to check this prefix against ourselves since we're the ones + // who proposed it + if(provider !== bestProvider) { + for(let currentProvide of providing) { + if(!provider.accept(multiIndex, prefix, currentProvide)) { + // console.log("bailing", provider); + solverInfo[providerIx]++; + accepted = false; + break; + } + } + } + providerIx++; + } + + // if we accepted this prefix and we're not on our final round, then + // we continue on to the next round by recursing with this prefix + if(accepted && rounds - providingLength > 0) { + joinRound(multiIndex, providers, prefix, rounds - providingLength, rows, options); + } else if(accepted) { + // otherwise if we're accepted, we have a valid result and we add it + // to our list of rows + rows.push(prefix.slice()); + } + // if we are only looking for a single result, e.g. for a NotScan, and we have + // a row, bail out of the evaluation + if(options.single && rows.length) return; + // since we're using the same prefix in our recursions, we have to clean + // up after ourselves so that parent rounds don't see our solved variables + // in their prefix. + for(let currentProvide of providing) { + prefix[currentProvide.id] = undefined; + } + } +} + +function preJoinAccept(multiIndex: MultiIndex, providers : ProposalProvider[], vars : Variable[], prefix: any[] = []) { + let ix = 0; + let presolved = 0; + for(let value of prefix) { + let solvingFor = vars[ix]; + if(value !== undefined && vars[ix] !== undefined) { + presolved++; + for(let provider of providers) { + if(!provider.accept(multiIndex, prefix, solvingFor, false, true)) { + return {accepted: false, presolved}; + } + } + } + ix++; + } + // we still need to do a single prejoin pass to make sure that any nots + // that may have no external dependencies are given a chance to end this + // evaluation + let fakeVar = new Variable(0); + for(let provider of providers) { + if(provider instanceof NotScan && !provider.accept(multiIndex, prefix, fakeVar, false, true)) { + return {accepted: false, presolved}; + } + } + return {accepted: true, presolved}; +} + +export interface JoinOptions { + single?: boolean, + acceptOnly?: boolean, + rows?: any[], + solverInfo?: any[] +} + +// Convenient function to kick off a join. We only care about vars here +// to determine how may rounds of generic join we need to do. Since we solve +// for one variable each round, it's the number of vars in the query. +export function join(multiIndex: MultiIndex, providers : ProposalProvider[], vars : Variable[], prefix: any[] = [], options: JoinOptions = {}) { + let rows = options.rows || []; + let {presolved, accepted} = preJoinAccept(multiIndex, providers, vars, prefix); + if(!accepted) return rows; + let rounds = 0; + for(let variable of vars) { + if(variable !== undefined) rounds++; + } + rounds = rounds - presolved; + if(presolved > 0 && rounds === 0) { + rows.push(prefix.slice()); + } else if(rounds === 0) { + for(let provider of providers) { + if(!provider.accept(multiIndex, prefix, null, true)) { + return rows; + } + } + rows.push(prefix.slice()); + } else { + joinRound(multiIndex, providers, prefix, rounds, rows, options); + } + return rows; +} + + + diff --git a/src/runtime/parser.ts b/src/runtime/parser.ts new file mode 100644 index 000000000..a69bae938 --- /dev/null +++ b/src/runtime/parser.ts @@ -0,0 +1,1436 @@ +//----------------------------------------------------------- +// Parser +//----------------------------------------------------------- + +import * as commonmark from "commonmark"; +import * as chev from "chevrotain"; +import * as join from "./join"; +import {parserErrors} from "./errors"; +import {buildDoc} from "./builder"; +import {time} from "./performance"; +var {Lexer} = chev; +var Token = chev.Token; + +//----------------------------------------------------------- +// Utils +//----------------------------------------------------------- + +function cleanString(str) { + let cleaned = str + .replace(/\\n/g, "\n") + .replace(/\\t/g, "\t") + .replace(/\\r/g, "\r") + .replace(/\\"/g, "\"") + .replace(/\\{/g, "{") + .replace(/\\}/g, "}"); + return cleaned; +} + +function toEnd(node) { + if(node instanceof Token) { + return node.endOffset + 1; + } + return node.endOffset; +} + +//----------------------------------------------------------- +// Markdown +//----------------------------------------------------------- + +let markdownParser = new commonmark.Parser(); + +function parseMarkdown(markdown: string, docId: string) { + let parsed = markdownParser.parse(markdown); + let walker = parsed.walker(); + var cur; + let tokenId = 0; + var text = []; + var extraInfo = {}; + var pos = 0; + var lastLine = 1; + var spans = []; + var context = []; + var blocks = []; + while(cur = walker.next()) { + let node = cur.node; + if(cur.entering) { + while(node.sourcepos && node.sourcepos[0][0] > lastLine) { + lastLine++; + pos++; + text.push("\n"); + } + if(node.type !== "text") { + context.push({node, start: pos}); + } + if(node.type == "text" || node.type === "code_block" || node.type == "code") { + text.push(node.literal); + pos += node.literal.length; + } + if(node.type == "softbreak") { + text.push("\n"); + pos += 1; + lastLine++; + context.pop(); + } + if(node.type == "code_block") { + let spanId = `${docId}|block|${tokenId++}`; + let start = context.pop().start; + node.id = spanId; + node.startOffset = start; + let type = node.type; + if(!node._isFenced) { + type = "indented_code_block"; + } else { + blocks.push(node); + } + spans.push(start, pos, node.type, spanId); + lastLine = node.sourcepos[1][0] + 1; + } + if(node.type == "code") { + let spanId = `${docId}|${tokenId++}`; + let start = context.pop().start; + spans.push(start, pos, node.type, spanId); + } + } else { + let info = context.pop(); + if(node !== info.node) { + throw new Error("Common mark is exiting a node that doesn't agree with the context stack"); + } + if(node.type == "emph" || node.type == "strong" || node.type == "link") { + let spanId = `${docId}|${tokenId++}`; + spans.push(info.start, pos, node.type, spanId); + if(node.type === "link") { + extraInfo[spanId] = {destination: node._destination}; + } + } else if(node.type == "heading" || node.type == "item") { + let spanId = `${docId}|${tokenId++}`; + spans.push(info.start, info.start, node.type, spanId); + extraInfo[spanId] = {level: node._level, listData: node._listData}; + } + } + } + return {text: text.join(""), spans, blocks, extraInfo}; +} + +//----------------------------------------------------------- +// Tokens +//----------------------------------------------------------- + +const breakChars = "@#\\.,\\(\\)\\[\\]\\{\\}⦑⦒:\\\""; + +// Markdown +export class DocContent extends Token { static PATTERN = /[^\n]+/; } +export class Fence extends Token { + static PATTERN = /```|~~~/; + static PUSH_MODE = "code"; +} +export class CloseFence extends Token { + static PATTERN = /```|~~~/; + static POP_MODE = true; +} + +// Comments +export class CommentLine extends Token { static PATTERN = /\/\/.*\n/; label = "comment"; static GROUP = "comments"; } + +// Operators +export class Equality extends Token { static PATTERN = /:|=/; label = "equality"; } +export class Comparison extends Token { static PATTERN = />=|<=|!=|>| { + self[name] = self.RULE(name, func); + } + let asValue = (node) => { + if(node.type === "constant" || node.type === "variable" || node.type === "parenthesis") { + return node; + } else if(node.variable) { + return node.variable; + } + throw new Error("Tried to get value of a node that is neither a constant nor a variable.\n\n" + JSON.stringify(node)); + } + let ifOutputs = (expression) => { + let outputs = []; + if(expression.type === "parenthesis") { + for(let item of expression.items) { + outputs.push(asValue(item)); + } + } else { + outputs.push(asValue(expression)); + } + return outputs; + } + + let makeNode = (type, node) => { + return self.block.makeNode(type, node); + } + + let blockStack = []; + let pushBlock = (blockId?) => { + let block; + let prev = blockStack[blockStack.length - 1]; + if(prev) { + block = prev.subBlock(); + } else { + block = new ParseBlock(blockId || "block"); + } + blockStack.push(block); + self.block = block; + return block; + } + + let popBlock = () => { + let popped = blockStack.pop(); + self.block = blockStack[blockStack.length - 1]; + return popped; + } + + //----------------------------------------------------------- + // Doc rules + //----------------------------------------------------------- + + rule("doc", () => { + let doc = { + full: [], + content: [], + blocks: [], + } + self.MANY(() => { + self.OR([ + {ALT: () => { + let content = self.CONSUME(DocContent); + doc.full.push(content); + doc.content.push(content); + }}, + {ALT: () => { + let block : any = self.SUBRULE(self.fencedBlock); + if(doc.content.length) { + block.name = doc.content[doc.content.length - 1].image; + } else { + block.name = "Unnamed block"; + } + doc.full.push(block); + doc.blocks.push(block); + }}, + ]) + }); + return doc; + }); + + rule("fencedBlock", () => { + self.CONSUME(Fence); + let block = self.SUBRULE(self.codeBlock); + let fence = self.CONSUME(CloseFence); + return block; + }); + + //----------------------------------------------------------- + // Blocks + //----------------------------------------------------------- + + rule("codeBlock", (blockId = "block") => { + blockStack = []; + let block = pushBlock(blockId); + self.MANY(() => { self.SUBRULE(self.section) }) + return popBlock(); + }) + + rule("section", () => { + return self.OR([ + {ALT: () => { return self.SUBRULE(self.searchSection) }}, + {ALT: () => { return self.SUBRULE(self.actionSection) }}, + {ALT: () => { return self.CONSUME(CommentLine); }}, + ]); + }); + + + //----------------------------------------------------------- + // Scope declaration + //----------------------------------------------------------- + + rule("scopeDeclaration", () => { + let scopes = []; + self.OR([ + {ALT: () => { + self.CONSUME(OpenParen); + self.AT_LEAST_ONE(() => { + let name: any = self.SUBRULE(self.name); + scopes.push(name.name); + }) + self.CONSUME(CloseParen); + }}, + {ALT: () => { + self.AT_LEAST_ONE2(() => { + let name: any = self.SUBRULE2(self.name); + scopes.push(name.name); + }) + }}, + ]); + return scopes; + }); + + + //----------------------------------------------------------- + // Search section + //----------------------------------------------------------- + + rule("searchSection", () => { + // @TODO fill in from + let from = []; + self.CONSUME(Search); + let scopes:any = ["session"]; + self.OPTION(() => { scopes = self.SUBRULE(self.scopeDeclaration) }) + self.activeScopes = scopes; + self.currentAction = "match"; + self.block.addSearchScopes(scopes); + let statements = []; + self.MANY(() => { + let statement: any = self.SUBRULE(self.statement); + if(statement) { + statements.push(statement); + statement.scopes = scopes; + } + }); + return makeNode("searchSection", {statements, scopes, from}); + }); + + rule("statement", () => { + return self.OR([ + {ALT: () => { return self.SUBRULE(self.comparison); }}, + {ALT: () => { return self.SUBRULE(self.notStatement); }}, + ]) + }); + + //----------------------------------------------------------- + // Action section + //----------------------------------------------------------- + + rule("actionSection", () => { + // @TODO fill in from + let from = []; + let action = self.CONSUME(Action).image; + let actionKey = action; + let scopes:any = ["session"]; + self.OPTION(() => { scopes = self.SUBRULE(self.scopeDeclaration) }) + self.activeScopes = scopes; + self.currentAction = action; + let statements = []; + self.MANY(() => { + let statement = self.SUBRULE(self.actionStatement, [actionKey]) as any; + if(statement) { + statements.push(statement); + statement.scopes = scopes; + } + }); + return makeNode("actionSection", {statements, scopes, from}); + }); + + + rule("actionStatement", (actionKey) => { + return self.OR([ + {ALT: () => { + let record = self.SUBRULE(self.record, [false, actionKey, "+="]); + return record; + }}, + {ALT: () => { return self.SUBRULE(self.actionEqualityRecord, [actionKey]); }}, + {ALT: () => { + let record = self.SUBRULE(self.actionOperation, [actionKey]); + self.block[actionKey](record); + return record; + }}, + {ALT: () => { return self.SUBRULE(self.actionLookup, [actionKey]); }}, + ]) + }); + + //----------------------------------------------------------- + // Action operations + //----------------------------------------------------------- + + rule("actionOperation", (actionKey) => { + return self.OR([ + {ALT: () => { return self.SUBRULE(self.recordOperation, [actionKey]) }}, + {ALT: () => { return self.SUBRULE(self.attributeOperation, [actionKey]) }}, + ]); + }); + + rule("attributeOperation", (actionKey) => { + let mutator = self.SUBRULE(self.attributeMutator) as any; + let {attribute, parent} = mutator; + return self.OR([ + {ALT: () => { + let variable = self.block.toVariable(`${attribute.image}|${attribute.startLine}|${attribute.startColumn}`, true); + let scan = makeNode("scan", {entity: parent, attribute: makeNode("constant", {value: attribute.image, from: [attribute]}), value: variable, scopes: self.activeScopes, from: [mutator]}); + self.block.addUsage(variable, scan); + self.block.scan(scan); + self.CONSUME(Merge); + let record = self.SUBRULE(self.record, [true, actionKey, "+=", undefined, variable]) as any; + record.variable = variable; + record.action = "<-"; + return record; + }}, + {ALT: () => { + let op = self.CONSUME(Set); + let none = self.CONSUME(None); + return makeNode("action", {action: "erase", entity: asValue(parent), attribute: attribute.image, from: [mutator, op, none]}); + }}, + {ALT: () => { + let op = self.CONSUME2(Set); + let value = self.SUBRULE(self.infix); + return makeNode("action", {action: op.image, entity: asValue(parent), attribute: attribute.image, value: asValue(value), from: [mutator, op, value]}); + }}, + {ALT: () => { + let op = self.CONSUME3(Set); + let value = self.SUBRULE2(self.record, [false, actionKey, "+=", parent]); + return makeNode("action", {action: op.image, entity: asValue(parent), attribute: attribute.image, value: asValue(value), from: [mutator, op, value]}); + }}, + {ALT: () => { + let variable = self.block.toVariable(`${attribute.image}|${attribute.startLine}|${attribute.startColumn}`, true); + let scan = makeNode("scan", {entity: parent, attribute: makeNode("constant", {value: attribute.image, from: [attribute]}), value: variable, scopes: self.activeScopes, from: [mutator]}); + self.block.addUsage(variable, scan); + self.block.scan(scan); + let op = self.CONSUME(Mutate); + let tag : any = self.SUBRULE(self.tag); + return makeNode("action", {action: op.image, entity: variable, attribute: "tag", value: makeNode("constant", {value: tag.tag, from: [tag]}), from: [mutator, op, tag]}); + }}, + {ALT: () => { + let op = self.CONSUME2(Mutate); + let value: any = self.SUBRULE2(self.actionAttributeExpression, [actionKey, op.image, parent]); + if(value.type === "record" && !value.extraProjection) { + value.extraProjection = [parent]; + } + if(value.type === "parenthesis") { + for(let item of value.items) { + if(item.type === "record" && !value.extraProjection) { + item.extraProjection = [parent]; + } + } + } + return makeNode("action", {action: op.image, entity: asValue(parent), attribute: attribute.image, value: asValue(value), from: [mutator, op, value]}); + }}, + ]) + }); + + rule("recordOperation", (actionKey) => { + let variable = self.SUBRULE(self.variable) as any; + return self.OR([ + {ALT: () => { + let set = self.CONSUME(Set); + let none = self.CONSUME(None); + return makeNode("action", {action: "erase", entity: asValue(variable), from: [variable, set, none]}); + }}, + {ALT: () => { + self.CONSUME(Merge); + let record = self.SUBRULE(self.record, [true, actionKey, "+=", undefined, variable]) as any; + record.needsEntity = true; + record.action = "<-"; + return record; + }}, + {ALT: () => { + let op = self.CONSUME(Mutate); + let tag : any = self.SUBRULE(self.tag); + return makeNode("action", {action: op.image, entity: asValue(variable), attribute: "tag", value: makeNode("constant", {value: tag.tag, from: [tag]}), from: [variable, op, tag]}); + }}, + ]) + }); + + rule("actionLookup", (actionKey) => { + let lookup = self.CONSUME(Lookup); + let record: any = self.SUBRULE(self.record, [true]); + let info: any = {}; + for(let attribute of record.attributes) { + info[attribute.attribute] = attribute.value; + } + let actionType = "+="; + self.OPTION(() => { + self.CONSUME(Set); + self.CONSUME(None); + if(info["value"] !== undefined) { + actionType = "-="; + } else { + actionType = "erase"; + } + }) + let action = makeNode("action", {action: actionType, entity: info.record, attribute: info.attribute, value: info.value, node: info.node, scopes: self.activeScopes, from: [lookup, record]}); + self.block[actionKey](action); + return action; + }); + + rule("actionAttributeExpression", (actionKey, action, parent) => { + return self.OR([ + {ALT: () => { return self.SUBRULE(self.record, [false, actionKey, action, parent]); }}, + {ALT: () => { return self.SUBRULE(self.infix); }}, + ]) + }) + + rule("actionEqualityRecord", (actionKey) => { + let variable = self.SUBRULE(self.variable); + self.CONSUME(Equality); + let record : any = self.SUBRULE(self.record, [true, actionKey, "+="]); + record.variable = variable; + self.block[actionKey](record); + return record; + }); + + //----------------------------------------------------------- + // Record + attribute + //----------------------------------------------------------- + + rule("record", (noVar = false, blockKey = "scan", action = false, parent?, passedVariable?) => { + let attributes = []; + let start = self.CONSUME(OpenBracket); + let from: NodeDependent[] = [start]; + let info: any = {attributes, action, scopes: self.activeScopes, from}; + if(parent) { + info.extraProjection = [parent]; + } + if(passedVariable) { + info.variable = passedVariable; + info.variable.nonProjecting = true; + } else if(!noVar) { + info.variable = self.block.toVariable(`record|${start.startLine}|${start.startColumn}`, true); + info.variable.nonProjecting = true; + } + let nonProjecting = false; + self.MANY(() => { + self.OR([ + {ALT: () => { + let attribute: any = self.SUBRULE(self.attribute, [false, blockKey, action, info.variable]); + // Inline handles attributes itself and so won't return any attribute for us to add + // to this object + if(!attribute) return; + + if(attribute.constructor === Array) { + for(let attr of attribute as any[]) { + attr.nonProjecting = nonProjecting; + attributes.push(attr); + from.push(attr); + } + } else { + attribute.nonProjecting = nonProjecting; + attributes.push(attribute); + from.push(attribute); + } + }}, + {ALT: () => { + nonProjecting = true; + let pipe = self.CONSUME(Pipe); + from.push(pipe); + return pipe; + }}, + ]); + }) + from.push(self.CONSUME(CloseBracket)); + let record : any = makeNode("record", info); + if(!noVar) { + self.block.addUsage(info.variable, record); + self.block[blockKey](record); + } + return record; + }); + + rule("attribute", (noVar, blockKey, action, recordVariable) => { + return self.OR([ + {ALT: () => { return self.SUBRULE(self.attributeEquality, [noVar, blockKey, action, recordVariable]); }}, + {ALT: () => { return self.SUBRULE(self.attributeComparison); }}, + {ALT: () => { return self.SUBRULE(self.attributeNot, [recordVariable]); }}, + {ALT: () => { return self.SUBRULE(self.singularAttribute); }}, + ]); + }); + + rule("singularAttribute", (forceGenerate) => { + return self.OR([ + {ALT: () => { + let tag : any = self.SUBRULE(self.tag); + return makeNode("attribute", {attribute: "tag", value: makeNode("constant", {value: tag.tag, from: [tag]}), from: [tag]}); + }}, + {ALT: () => { + let variable : any = self.SUBRULE(self.variable, [forceGenerate]); + return makeNode("attribute", {attribute: variable.from[0].image, value: variable, from: [variable]}); + }}, + ]); + }); + + rule("attributeMutator", () => { + let scans = []; + let entity, attribute, value; + let needsEntity = true; + let from = []; + entity = self.SUBRULE(self.variable); + let dot = self.CONSUME(Dot); + from.push(entity, dot); + self.MANY(() => { + attribute = self.CONSUME(Identifier); + from.push(attribute); + from.push(self.CONSUME2(Dot)); + value = self.block.toVariable(`${attribute.image}|${attribute.startLine}|${attribute.startColumn}`, true); + self.block.addUsage(value, attribute); + let scopes = self.activeScopes; + if(self.currentAction !== "match") { + scopes = self.block.searchScopes; + } + let scan = makeNode("scan", {entity, attribute: makeNode("constant", {value: attribute.image, from: [value]}), value, needsEntity, scopes, from: [entity, dot, attribute]}); + self.block.scan(scan); + needsEntity = false; + entity = value; + }); + attribute = self.CONSUME2(Identifier); + from.push(attribute); + return makeNode("attributeMutator", {attribute: attribute, parent: entity, from}); + }); + + rule("attributeAccess", () => { + let scans = []; + let entity, attribute, value; + let needsEntity = true; + entity = self.SUBRULE(self.variable); + let parentId = entity.name; + self.AT_LEAST_ONE(() => { + let dot = self.CONSUME(Dot); + attribute = self.CONSUME(Identifier); + parentId = `${parentId}|${attribute.image}`; + value = self.block.toVariable(parentId, true); + self.block.addUsage(value, attribute); + let scopes = self.activeScopes; + if(self.currentAction !== "match") { + scopes = self.block.searchScopes; + } + let scan = makeNode("scan", {entity, attribute: makeNode("constant", {value: attribute.image, from: [attribute]}), value, needsEntity, scopes, from: [entity, dot, attribute]}); + self.block.scan(scan); + needsEntity = false; + entity = value; + }); + return value; + }); + + rule("attributeEquality", (noVar, blockKey, action, parent) => { + let attributes = []; + let autoIndex = 1; + let attributeNode; + let attribute: any = self.OR([ + {ALT: () => { + attributeNode = self.CONSUME(Identifier); + return attributeNode.image; + }}, + {ALT: () => { + attributeNode = self.CONSUME(Num); + return parseFloat(attributeNode.image) as any; + }} + ]); + let equality = self.CONSUME(Equality); + let result : any; + self.OR2([ + {ALT: () => { + result = self.SUBRULE(self.infix); + }}, + {ALT: () => { + result = self.SUBRULE(self.record, [noVar, blockKey, action, parent]); + self.MANY(() => { + autoIndex++; + let record : any = self.SUBRULE2(self.record, [noVar, blockKey, action, parent]); + record.attributes.push(makeNode("attribute", {attribute: "eve-auto-index", value: makeNode("constant", {value: autoIndex, from: [record]}), from: [record]})); + attributes.push(makeNode("attribute", {attribute, value: asValue(record), from: [attributeNode, equality, record]})); + }) + if(autoIndex > 1) { + result.attributes.push(makeNode("attribute", {attribute: "eve-auto-index", value: makeNode("constant", {value: 1, from: [result]}), from: [result]})); + } + }}, + ]); + attributes.push(makeNode("attribute", {attribute, value: asValue(result), from: [attributeNode, equality, result]})) + return attributes; + }); + + rule("attributeComparison", () => { + let attribute = self.CONSUME(Identifier); + let comparator = self.CONSUME(Comparison); + let result = self.SUBRULE(self.expression); + let variable = self.block.toVariable(`attribute|${attribute.startLine}|${attribute.startColumn}`, true); + let expression = makeNode("expression", {op: comparator.image, args: [asValue(variable), asValue(result)], from: [attribute, comparator, result]}) + self.block.addUsage(variable, expression); + self.block.expression(expression); + return makeNode("attribute", {attribute: attribute.image, value: variable, from: [attribute, comparator, expression]}); + }); + + rule("attributeNot", (recordVariable) => { + let block = pushBlock(); + block.type = "not"; + let not = self.CONSUME(Not); + let start = self.CONSUME(OpenParen); + let attribute: any = self.OR([ + {ALT: () => { return self.SUBRULE(self.attributeComparison); }}, + {ALT: () => { return self.SUBRULE(self.singularAttribute, [true]); }}, + ]); + let end = self.CONSUME(CloseParen); + // we have to add a record for this guy + let scan : any = makeNode("scan", {entity: recordVariable, attribute: makeNode("constant", {value: attribute.attribute, from: [attribute]}), value: attribute.value, needsEntity: true, scopes: self.activeScopes, from: [attribute]}); + block.variables[recordVariable.name] = recordVariable; + block.scan(scan); + block.from = [not, start, attribute, end]; + block.startOffset = not.startOffset; + block.endOffset = toEnd(end); + popBlock(); + self.block.scan(block); + return; + }); + + //----------------------------------------------------------- + // Name and tag + //----------------------------------------------------------- + + rule("name", () => { + let at = self.CONSUME(Name); + let name = self.CONSUME(Identifier); + return makeNode("name", {name: name.image, from: [at, name]}); + }); + + rule("tag", () => { + let hash = self.CONSUME(Tag); + let tag = self.CONSUME(Identifier); + return makeNode("tag", {tag: tag.image, from: [hash, tag]}); + }); + + //----------------------------------------------------------- + // Function + //----------------------------------------------------------- + + rule("functionRecord", (): any => { + let name = self.OR([ + {ALT: () => { return self.CONSUME(FunctionIdentifier); }}, + {ALT: () => { return self.CONSUME(Lookup); }} + ]); + let record: any = self.SUBRULE(self.record, [true]); + if(name.image === "lookup") { + let info: any = {}; + for(let attribute of record.attributes) { + info[attribute.attribute] = attribute.value; + } + let scan = makeNode("scan", {entity: info.record, attribute: info.attribute, value: info.value, node: info.node, scopes: self.activeScopes, from: [name, record]}); + self.block.scan(scan); + return scan; + } else { + let variable = self.block.toVariable(`return|${name.startLine}|${name.startColumn}`, true); + let functionRecord = makeNode("functionRecord", {op: name.image, record, variable, from: [name, record]}); + self.block.addUsage(variable, functionRecord); + self.block.expression(functionRecord); + return functionRecord; + } + }); + + //----------------------------------------------------------- + // Comparison + //----------------------------------------------------------- + + rule("comparison", (nonFiltering) : any => { + let left = self.SUBRULE(self.expression); + let from = [left]; + let rights = []; + self.MANY(() => { + let comparator = self.OR([ + {ALT: () => { return self.CONSUME(Comparison); }}, + {ALT: () => { return self.CONSUME(Equality); }} + ]); + let value = self.OR2([ + {ALT: () => { return self.SUBRULE2(self.expression); }}, + {ALT: () => { return self.SUBRULE(self.ifExpression); }} + ]); + from.push(comparator, value); + rights.push({comparator, value}); + }) + if(rights.length) { + let expressions = []; + let curLeft: any = left; + for(let pair of rights) { + let {comparator, value} = pair; + let expression = null; + // if this is a nonFiltering comparison, then we return an expression + // with a variable for its return value + if(nonFiltering) { + let variable = self.block.toVariable(`comparison|${comparator.startLine}|${comparator.startColumn}`, true); + expression = makeNode("expression", {variable, op: comparator.image, args: [asValue(curLeft), asValue(value)], from: [curLeft, comparator, value]}); + self.block.addUsage(variable, expression); + self.block.expression(expression); + } else if(comparator instanceof Equality) { + if(value.type === "ifExpression") { + value.outputs = ifOutputs(left); + self.block.scan(value); + } else if(value.type === "functionRecord" && curLeft.type === "parenthesis") { + value.returns = curLeft.items.map(asValue); + self.block.equality(asValue(value.returns[0]), asValue(value)); + } else if(curLeft.type === "parenthesis") { + throw new Error("Left hand parenthesis without an if or function on the right"); + } else { + self.block.equality(asValue(curLeft), asValue(value)); + } + } else { + expression = makeNode("expression", {op: comparator.image, args: [asValue(curLeft), asValue(value)], from: [curLeft, comparator, value]}); + self.block.expression(expression); + } + curLeft = value; + if(expression) { + expressions.push(expression); + } + } + return makeNode("comparison", {expressions, from}); + }; + return left; + }); + + //----------------------------------------------------------- + // Special Forms + //----------------------------------------------------------- + + rule("notStatement", () => { + let block = pushBlock(); + block.type = "not"; + let from: NodeDependent[] = [ + self.CONSUME(Not), + self.CONSUME(OpenParen), + ]; + self.MANY(() => { + from.push(self.SUBRULE(self.statement) as ParseNode); + }); + from.push(self.CONSUME(CloseParen)); + popBlock(); + block.from = from; + block.startOffset = from[0].startOffset; + block.endOffset = toEnd(from[from.length - 1]); + self.block.scan(block); + return; + }); + + rule("isExpression", () => { + let op = self.CONSUME(Is); + let from: NodeDependent[] = [ + op, + self.CONSUME(OpenParen) + ] + let expressions = []; + self.MANY(() => { + let comparison: any = self.SUBRULE(self.comparison, [true]); + for(let expression of comparison.expressions) { + from.push(expression as ParseNode); + expressions.push(asValue(expression)); + } + }); + from.push(self.CONSUME(CloseParen)); + let variable = self.block.toVariable(`is|${op.startLine}|${op.startColumn}`, true); + let is = makeNode("expression", {variable, op: "and", args: expressions, from}); + self.block.addUsage(variable, is); + self.block.expression(is); + return is; + }); + + //----------------------------------------------------------- + // If ... then + //----------------------------------------------------------- + + rule("ifExpression", () => { + let branches = []; + let from = branches; + branches.push(self.SUBRULE(self.ifBranch)); + self.MANY(() => { + branches.push(self.OR([ + {ALT: () => { return self.SUBRULE2(self.ifBranch); }}, + {ALT: () => { return self.SUBRULE(self.elseIfBranch); }}, + ])); + }); + self.OPTION(() => { + branches.push(self.SUBRULE(self.elseBranch)); + }); + return makeNode("ifExpression", {branches, from}); + }); + + rule("ifBranch", () => { + let block = pushBlock(); + let from: NodeDependent[] = [ + self.CONSUME(If) + ] + self.AT_LEAST_ONE(() => { + let statement = self.SUBRULE(self.statement) as ParseNode; + if(statement) { + from.push(statement); + } + }) + from.push(self.CONSUME(Then)); + let expression = self.SUBRULE(self.expression) as ParseNode; + from.push(expression); + block.startOffset = from[0].startOffset; + block.endOffset = toEnd(from[from.length - 1]); + popBlock(); + return makeNode("ifBranch", {block, outputs: ifOutputs(expression), exclusive: false, from}); + }); + + rule("elseIfBranch", () => { + let block = pushBlock(); + let from: NodeDependent[] = [ + self.CONSUME(Else), + self.CONSUME(If), + ] + self.AT_LEAST_ONE(() => { + let statement = self.SUBRULE(self.statement) as ParseNode; + if(statement) { + from.push(statement); + } + }) + from.push(self.CONSUME(Then)); + let expression = self.SUBRULE(self.expression) as ParseNode; + from.push(expression); + block.startOffset = from[0].startOffset; + block.endOffset = toEnd(from[from.length - 1]); + popBlock(); + return makeNode("ifBranch", {block, outputs: ifOutputs(expression), exclusive: true, from}); + }); + + rule("elseBranch", () => { + let block = pushBlock(); + let from: NodeDependent[] = [self.CONSUME(Else)]; + let expression = self.SUBRULE(self.expression) as ParseNode; + from.push(expression); + block.startOffset = from[0].startOffset; + block.endOffset = toEnd(from[from.length - 1]); + popBlock(); + return makeNode("ifBranch", {block, outputs: ifOutputs(expression), exclusive: true, from}); + }); + + //----------------------------------------------------------- + // Infix and operator precedence + //----------------------------------------------------------- + + rule("infix", () => { + return self.SUBRULE(self.addition); + }); + + rule("addition", () : any => { + let left = self.SUBRULE(self.multiplication); + let from = [left]; + let ops = []; + self.MANY(function() { + let op = self.CONSUME(AddInfix); + let right = self.SUBRULE2(self.multiplication); + from.push(op, right); + ops.push({op, right}) + }); + if(!ops.length) { + return left; + } else { + let expressions = []; + let curVar; + let curLeft = left; + for(let pair of ops) { + let {op, right} = pair; + curVar = self.block.toVariable(`addition|${op.startLine}|${op.startColumn}`, true); + let expression = makeNode("expression", {op: op.image, args: [asValue(curLeft), asValue(right)], variable: curVar, from: [curLeft, op, right]}); + expressions.push(expression); + self.block.addUsage(curVar, expression); + self.block.expression(expression) + curLeft = expression; + } + return makeNode("addition", {expressions, variable: curVar, from}); + } + }); + + rule("multiplication", () : any => { + let left = self.SUBRULE(self.infixValue); + let from = [left]; + let ops = []; + self.MANY(function() { + let op = self.CONSUME(MultInfix); + let right = self.SUBRULE2(self.infixValue); + from.push(op, right); + ops.push({op, right}) + }); + if(!ops.length) { + return left; + } else { + let expressions = []; + let curVar; + let curLeft = left; + for(let pair of ops) { + let {op, right} = pair; + curVar = self.block.toVariable(`addition|${op.startLine}|${op.startColumn}`, true); + let expression = makeNode("expression", {op: op.image, args: [asValue(curLeft), asValue(right)], variable: curVar, from: [curLeft, op, right]}); + expressions.push(expression); + self.block.addUsage(curVar, expression); + self.block.expression(expression) + curLeft = expression; + } + return makeNode("multiplication", {expressions, variable: curVar, from}); + } + }); + + rule("parenthesis", () => { + let items = []; + let from = []; + from.push(self.CONSUME(OpenParen)); + self.AT_LEAST_ONE(() => { + let item = self.SUBRULE(self.expression); + items.push(asValue(item)); + from.push(item); + }) + from.push(self.CONSUME(CloseParen)); + if(items.length === 1) { + return items[0]; + } + return makeNode("parenthesis", {items, from}); + }); + + rule("infixValue", () => { + return self.OR([ + {ALT: () => { return self.SUBRULE(self.attributeAccess); }}, + {ALT: () => { return self.SUBRULE(self.functionRecord); }}, + {ALT: () => { return self.SUBRULE(self.isExpression); }}, + {ALT: () => { return self.SUBRULE(self.variable); }}, + {ALT: () => { return self.SUBRULE(self.value); }}, + {ALT: () => { return self.SUBRULE(self.parenthesis); }}, + ]); + }) + + //----------------------------------------------------------- + // Expression + //----------------------------------------------------------- + + rule("expression", () => { + let blockKey, action; + if(self.currentAction !== "match") { + blockKey = self.currentAction; + action = "+="; + } + return self.OR([ + {ALT: () => { return self.SUBRULE(self.infix); }}, + {ALT: () => { return self.SUBRULE(self.record, [false, blockKey, action]); }}, + ]); + }); + + //----------------------------------------------------------- + // Variable + //----------------------------------------------------------- + + rule("variable", (forceGenerate = false) => { + let token = self.CONSUME(Identifier); + let name = token.image; + if(forceGenerate) { + name = `${token.image}-${token.startLine}-${token.startColumn}`; + } + let variable = self.block.toVariable(name, forceGenerate); + self.block.addUsage(variable, token); + return variable; + }); + + //----------------------------------------------------------- + // Values + //----------------------------------------------------------- + + rule("stringInterpolation", () : any => { + let args = []; + let start = self.CONSUME(OpenString); + let from: NodeDependent[] = [start]; + self.MANY(() => { + let arg = self.OR([ + {ALT: () => { + let str = self.CONSUME(StringChars); + return makeNode("constant", {value: cleanString(str.image), from: [str]}); + }}, + {ALT: () => { + self.CONSUME(StringEmbedOpen); + let expression = self.SUBRULE(self.infix); + self.CONSUME(StringEmbedClose); + return expression; + }}, + ]); + args.push(asValue(arg)); + from.push(arg as ParseNode); + }); + from.push(self.CONSUME(CloseString)); + if(args.length === 1 && args[0].type === "constant") { + return args[0]; + } + let variable = self.block.toVariable(`concat|${start.startLine}|${start.startColumn}`, true); + let expression = makeNode("expression", {op: "concat", args, variable, from}); + self.block.addUsage(variable, expression); + self.block.expression(expression); + return expression; + }); + + rule("value", () => { + return self.OR([ + {ALT: () => { return self.SUBRULE(self.stringInterpolation) }}, + {ALT: () => { return self.SUBRULE(self.num) }}, + {ALT: () => { return self.SUBRULE(self.bool) }}, + ]) + }) + + rule("bool", () => { + let value = self.CONSUME(Bool); + return makeNode("constant", {value: value.image === "true", from: [value]}); + }) + + rule("num", () => { + let num = self.CONSUME(Num); + return makeNode("constant", {value: parseFloat(num.image), from: [num]}) ; + }); + + //----------------------------------------------------------- + // Chevrotain analysis + //----------------------------------------------------------- + + Parser.performSelfAnalysis(this); + } +} + +//----------------------------------------------------------- +// Public API +//----------------------------------------------------------- + +export function nodeToBoundaries(node, offset = 0) { + return [node.startOffset, toEnd(node)]; +} + +let eveParser = new Parser([]); + +export function parseBlock(block, blockId, offset = 0, spans = [], extraInfo = {}) { + let start = time(); + let lex: any = EveBlockLexer.tokenize(block); + let token: any; + let tokenIx = 0; + for(token of lex.tokens) { + let tokenId = `${blockId}|token|${tokenIx++}`; + token.id = tokenId; + token.startOffset += offset; + spans.push(token.startOffset, token.startOffset + token.image.length, token.label, tokenId); + } + for(token of lex.groups.comments) { + let tokenId = `${blockId}|token|${tokenIx++}`; + token.id = tokenId; + token.startOffset += offset; + spans.push(token.startOffset, token.startOffset + token.image.length, token.label, tokenId); + } + eveParser.input = lex.tokens; + // The parameters here are a strange quirk of how Chevrotain works, I believe the + // 1 tells chevrotain what level the rule is starting at, we then pass our params + // to the codeBlock parser function as an array + let results = eveParser.codeBlock(1, [blockId]); + if(results) { + results.start = offset; + results.startOffset = offset; + results.tokens = lex.tokens; + for(let scan of results.scanLike) { + let type = "scan-boundary"; + if(scan.type === "record") { + type = "record-boundary"; + } + spans.push(scan.startOffset, scan.endOffset, type, scan.id); + } + for(let action of results.binds) { + let type = "action-boundary"; + if(action.type === "record") { + type = "action-record-boundary"; + } + spans.push(action.startOffset, action.endOffset, type, action.id); + extraInfo[action.id] = {kind: "bind"}; + } + for(let action of results.commits) { + let type = "action-boundary"; + if(action.type === "record") { + type = "action-record-boundary"; + } + spans.push(action.startOffset, action.endOffset, type, action.id); + extraInfo[action.id] = {kind: "commits"}; + } + } + let errors = parserErrors(eveParser.errors, {blockId, blockStart: offset, spans, extraInfo, tokens: lex.tokens}); + lex.groups.comments.length = 0; + return { + results, + lex, + time: time(start), + errors, + } +} + +let docIx = 0; +export function parseDoc(doc, docId = `doc|${docIx++}`) { + let start = time(); + let {text, spans, blocks, extraInfo} = parseMarkdown(doc, docId); + let parsedBlocks = []; + let allErrors = []; + for(let block of blocks) { + extraInfo[block.id] = {info: block.info}; + if(block.info !== "" && block.info.indexOf("eve") === -1) continue; + let {results, lex, errors} = parseBlock(block.literal, block.id, block.startOffset, spans, extraInfo); + // if this block is disabled, we want the parsed spans and such, but we don't want + // the block to be in the set sent to the builder + if(block.info.indexOf("disabled") > -1) { + extraInfo[block.id].disabled = true; + } else if(errors.length) { + allErrors.push(errors); + } else { + results.endOffset = block.endOffset; + parsedBlocks.push(results); + } + } + return { + results: {blocks: parsedBlocks, text, spans, extraInfo}, + time: time(start), + errors: allErrors, + } +} diff --git a/src/runtime/performance.ts b/src/runtime/performance.ts new file mode 100644 index 000000000..83a070dff --- /dev/null +++ b/src/runtime/performance.ts @@ -0,0 +1,210 @@ +//--------------------------------------------------------------------- +// Performance +//--------------------------------------------------------------------- + +export class NoopPerformanceTracker { + storeTime: number; + storeCalls: number; + + lookupTime: number; + lookupCalls: number; + + blockTime: any; + blockTimeMax: any; + blockTimeMin: any; + blockCalls: any; + + sendTime: number; + sendCalls: number; + + fixpointTime: number; + fixpointCalls: number; + + blockCheckTime: number; + blockCheckCalls: number; + + time: (start?) => number | number[] | string; + + constructor() { + this.time = () => 0; + } + reset() { } + lookup(start) { } + store(start) { } + block(name, start) { } + send(start) { } + blockCheck(start) { } + fixpoint(start) { } + asObject(blockMap: Object): any {} + report() { } +} + +export class PerformanceTracker extends NoopPerformanceTracker { + + time: (start?) => number | number[] | string; + + constructor() { + super(); + this.reset(); + this.time = time; + } + + reset() { + this.storeTime = 0; + this.storeCalls = 0; + this.lookupTime = 0; + this.lookupCalls = 0; + this.sendTime = 0; + this.sendCalls = 0; + this.fixpointTime = 0; + this.fixpointCalls = 0; + this.blockCheckTime = 0; + this.blockCheckCalls = 0; + this.blockTime = {}; + this.blockTimeMax = {}; + this.blockTimeMin = {}; + this.blockCalls = {}; + } + + lookup(start) { + this.lookupTime += time(start) as number; + this.lookupCalls++; + } + + store(start) { + this.storeTime += time(start) as number; + this.storeCalls++; + } + + block(name, start) { + if(this.blockTime[name] === undefined) { + this.blockTime[name] = 0; + this.blockCalls[name] = 0; + this.blockTimeMax[name] = -Infinity; + this.blockTimeMin[name] = Infinity; + } + let total = time(start) as number; + this.blockTime[name] += total; + this.blockCalls[name]++; + if(total > this.blockTimeMax[name]) { + this.blockTimeMax[name] = total; + } + if(total < this.blockTimeMin[name]) { + this.blockTimeMin[name] = total; + } + } + + send(start) { + this.sendTime += time(start) as number; + this.sendCalls++; + } + + blockCheck(start) { + this.blockCheckTime += time(start) as number; + this.blockCheckCalls++; + } + + fixpoint(start) { + this.fixpointTime += time(start) as number; + this.fixpointCalls++; + } + + asObject(blockMap: Object) { + let info = {}; + let blockInfo = {}; + let blocks = Object.keys(this.blockTime); + blocks.sort((a,b) => { + return this.blockTime[b] - this.blockTime[a]; + }); + for(let name of blocks) { + if(!blockMap[name]) continue; + let time = this.blockTime[name]; + let calls = this.blockCalls[name]; + let max = this.blockTimeMax[name]; + let min = this.blockTimeMin[name]; + let avg = time / calls; + let color = avg > 5 ? "red" : (avg > 1 ? "orange" : "green"); + let fixedpointPercent = (time * 100 / this.fixpointTime); + blockInfo[name] = { + time, calls, min, max, avg, color, percentFixpoint: fixedpointPercent + } + } + let fixpoint = { + time: this.fixpointTime, + count: this.fixpointCalls, + avg: this.fixpointTime / this.fixpointCalls, + } + return {fixpoint, blocks: blockInfo}; + } + + report() { + console.log("------------------ Performance --------------------------") + console.log("%cFixpoint", "font-size:14pt; margin:10px 0;"); + console.log(""); + console.log(` Time: ${this.fixpointTime}`) + console.log(` Count: ${this.fixpointCalls}`) + console.log(` Average time: ${this.fixpointTime / this.fixpointCalls}`) + console.log(""); + console.log("%cBlocks", "font-size:16pt;"); + console.log(""); + let blocks = Object.keys(this.blockTime); + blocks.sort((a,b) => { + return this.blockTime[b] - this.blockTime[a]; + }); + for(let name of blocks) { + let time = this.blockTime[name]; + let calls = this.blockCalls[name]; + let max = this.blockTimeMax[name]; + let min = this.blockTimeMin[name]; + let avg = time / calls; + let color = avg > 5 ? "red" : (avg > 1 ? "orange" : "green"); + console.log(` %c${name.substring(0,40)}`, "font-weight:bold;"); + console.log(` Time: ${time.toFixed(4)}`); + console.log(` Calls: ${calls}`); + console.log(` Max: ${max.toFixed(4)}`); + console.log(` Min: ${min.toFixed(4)}`); + console.log(` Average: %c${avg.toFixed(4)}`, `color:${color};`); + console.log(` Fixpoint: %c${(time * 100 / this.fixpointTime).toFixed(1)}%`, `color:${color};`); + console.log(""); + } + console.log(""); + console.log("Block check") + console.log(""); + console.log(` Time: ${this.blockCheckTime}`) + console.log(` Count: ${this.blockCheckCalls}`) + console.log(` Average time: ${this.blockCheckTime / this.blockCheckCalls}`) + console.log(""); + console.log("Lookup") + console.log(""); + console.log(` Time: ${this.lookupTime}`) + console.log(` Count: ${this.lookupCalls}`) + console.log(` Average time: ${this.lookupTime / this.lookupCalls}`) + console.log(""); + console.log("Store") + console.log(""); + console.log(` Time: ${this.storeTime}`) + console.log(` Count: ${this.storeCalls}`) + console.log(` Average store: ${this.storeTime / this.storeCalls}`) + console.log(""); + console.log("send"); + console.log(""); + console.log(` Time: ${this.sendTime}`) + console.log(` Count: ${this.sendCalls}`) + console.log(` Average time: ${this.sendTime / this.sendCalls}`) + } +} + +export var time; +if(global.process) { + time = function(start?): number | number[] | string { + if ( !start ) return process.hrtime(); + let end = process.hrtime(start); + return ((end[0]*1000) + (end[1]/1000000)).toFixed(3); + } +} else { + time = function(start?): number | number[] | string { + if ( !start ) return performance.now(); + let end = performance.now(); + return end - start; + } +} diff --git a/src/runtime/providers/aggregate.ts b/src/runtime/providers/aggregate.ts new file mode 100644 index 000000000..ac17a44f0 --- /dev/null +++ b/src/runtime/providers/aggregate.ts @@ -0,0 +1,183 @@ +//--------------------------------------------------------------------- +// Aggregate providers +//--------------------------------------------------------------------- + +import {Constraint, isVariable, resolve, toValue} from "../join"; +import * as providers from "./index"; + +export abstract class Aggregate extends Constraint { + static isAggregate = true; + static AttributeMapping = { + "value": 0, + "given": 1, + "per": 2, + } + + projectionVars: any[]; + groupVars: any[]; + resolvedGroup: any[]; + resolvedProjection: any[]; + resolvedAggregate: {group: any[], projection: any[], value: any}; + aggregateResults: any; + value: any; + + constructor(id: string, args: any[], returns: any[]) { + super(id, args, returns); + let [value, given, per] = args; + if(given === undefined) { + this.projectionVars = []; + } else if(isVariable(given)) { + this.projectionVars = [given]; + } else { + this.projectionVars = given; + } + if(per === undefined) { + this.groupVars = []; + } else if(isVariable(per)) { + this.groupVars = [per]; + } else { + this.groupVars = per; + } + this.value = value; + this.resolvedGroup = []; + this.resolvedProjection = []; + this.resolvedAggregate = {group: this.resolvedGroup, projection: this.resolvedProjection, value: undefined}; + this.aggregateResults = {}; + } + + resolveAggregate(prefix) { + resolve(this.projectionVars, prefix, this.resolvedProjection) + resolve(this.groupVars, prefix, this.resolvedGroup) + let resolved = this.resolvedAggregate; + resolved.value = toValue(this.value, prefix); + return resolved; + } + + aggregate(rows: any[]) { + let groupKeys = []; + let groups = {}; + for(let row of rows) { + let {group, projection, value} = this.resolveAggregate(row); + let groupKey = "[]"; + if(group.length !== 0) { + groupKey = JSON.stringify(group); + } + let groupValues = groups[groupKey]; + if(groupValues === undefined) { + groupKeys.push(groupKey); + groupValues = groups[groupKey] = {}; + } + let projectionKey = JSON.stringify(projection); + if(groupValues[projectionKey] === undefined) { + groupValues[projectionKey] = true; + this.adjustAggregate(groupValues, value, projection); + } + } + for(let key of groupKeys) { + this.finalizeGroup(groups[key]); + } + this.aggregateResults = groups; + return groups; + } + + resolveProposal(proposal, prefix) { + if(proposal.index) { + return [proposal.index.result]; + } + return []; + } + + test(prefix) { + let {group} = this.resolveAggregate(prefix); + let resultGroup = this.aggregateResults[JSON.stringify(group)]; + if(resultGroup !== undefined) { + let returns = resolve(this.returns, prefix, this.resolvedReturns); + return returns[0] === resultGroup.result; + } + } + + getProposal(multiIndex, proposed, prefix) { + let {group} = this.resolveAggregate(prefix); + let resultGroup = this.aggregateResults[JSON.stringify(group)]; + let proposal = this.proposalObject; + if(resultGroup) { + proposal.index = resultGroup + proposal.providing = proposed; + proposal.cardinality = 1; + } else { + proposal.index = undefined; + proposal.providing = proposed; + proposal.cardinality = 0; + } + return proposal; + } + + finalizeGroup(group) {} + + abstract adjustAggregate(group, value, projection): any; +} + +export class Sum extends Aggregate { + adjustAggregate(group, value, projection) { + if(group.result === undefined) { + group.result = value; + } else { + group.result += value; + } + return group.result; + } +} + +export class Count extends Aggregate { + adjustAggregate(group, value, projection) { + if(group.result === undefined) { + group.result = 1; + } else { + group.result += 1; + } + return group.result; + } +} + +export class Average extends Aggregate { + adjustAggregate(group, value, projection) { + if(group.count === undefined) { + group.count = 1; + group.sum = value; + group.result = group.sum / group.count; + } else { + group.count += 1; + group.sum += value; + group.result = group.sum / group.count; + } + return group.result; + } +} + +export class Min extends Aggregate { + adjustAggregate(group, value, projection) { + if(group.result === undefined) { + group.result = value; + } else if(value < group.result) { + group.result = value; + } + return group.result; + } +} + +export class Max extends Aggregate { + adjustAggregate(group, value, projection) { + if(group.result === undefined) { + group.result = value; + } else if(value > group.result) { + group.result = value; + } + return group.result; + } +} + +providers.provide("sum", Sum); +providers.provide("count", Count); +providers.provide("average", Average); +providers.provide("min", Min); +providers.provide("max", Max); diff --git a/src/runtime/providers/index.ts b/src/runtime/providers/index.ts new file mode 100644 index 000000000..a6fd33b75 --- /dev/null +++ b/src/runtime/providers/index.ts @@ -0,0 +1,10 @@ +var providers = { } + +export function provide(name, klass) { + providers[name] = klass; +} + +export function get(name): any { + return providers[name]; +} + diff --git a/src/runtime/providers/logical.ts b/src/runtime/providers/logical.ts new file mode 100644 index 000000000..83dd877fc --- /dev/null +++ b/src/runtime/providers/logical.ts @@ -0,0 +1,181 @@ +//--------------------------------------------------------------------- +// Logical providers +//--------------------------------------------------------------------- + +import {Constraint} from "../join"; +import * as providers from "./index"; + +abstract class BooleanOperation extends Constraint { + resolveProposal(proposal, prefix) { + let {args} = this.resolve(prefix); + return [this.compare(args[0], args[1])]; + } + + getProposal(tripleIndex, proposed, prefix) { + if(this.returns.length) { + let proposal = this.proposalObject; + proposal.providing = proposed; + proposal.cardinality = 1; + return proposal; + } + return; + } + + test(prefix) { + let {args, returns} = this.resolve(prefix); + let result = this.compare(args[0], args[1]); + if(returns.length) { + return result === returns[0]; + } + return result; + } + + abstract compare(a,b): boolean; +} + +class Equal extends BooleanOperation { + compare(a, b) { return a === b; } +} + +class NotEqual extends BooleanOperation { + compare(a, b) { return a !== b; } +} + +class GreaterThan extends BooleanOperation { + compare(a, b) { return a > b; } +} + +class LessThan extends BooleanOperation { + compare(a, b) { return a < b; } +} + +class GreaterThanEqualTo extends BooleanOperation { + compare(a, b) { return a >= b; } +} + +class LessThanEqualTo extends BooleanOperation { + compare(a, b) { return a <= b; } +} + +class AssertValue extends Constraint { + resolveProposal(proposal, prefix) { + let {args, returns} = this.resolve(prefix); + return [args[0]]; + } + + test(prefix) { + let {args, returns} = this.resolve(prefix); + return args[0] === returns[0]; + } + + getProposal(tripleIndex, proposed, prefix) { + let proposal = this.proposalObject; + proposal.providing = proposed; + proposal.cardinality = 1; + return proposal; + } +} + +class And extends Constraint { + resolveProposal(proposal, prefix) { + let {args} = this.resolve(prefix); + let result = true; + for(let arg of args) { + if(arg === false) { + result = false; + break; + } + } + return [result]; + } + + test(prefix) { + let {args, returns} = this.resolve(prefix); + let result = true; + for(let arg of args) { + if(arg === false) { + result = false; + break; + } + } + return result === returns[0]; + } + + getProposal(tripleIndex, proposed, prefix) { + let proposal = this.proposalObject; + proposal.providing = proposed; + proposal.cardinality = 1; + return proposal; + } +} + + +class Or extends Constraint { + // To resolve a proposal, we concatenate our resolved args + resolveProposal(proposal, prefix) { + let {args} = this.resolve(prefix); + let result = false; + for(let arg of args) { + if(arg !== false) { + result = true; + break; + } + } + return [result]; + } + + // We accept a prefix if the return is equivalent to concatentating + // all the args + test(prefix) { + let {args, returns} = this.resolve(prefix); + let result = false; + for(let arg of args) { + if(arg !== false) { + result = true; + break; + } + } + return result === returns[0]; + } + + // concat always returns cardinality 1 + getProposal(tripleIndex, proposed, prefix) { + let proposal = this.proposalObject; + proposal.providing = proposed; + proposal.cardinality = 1; + return proposal; + } +} + + +class Toggle extends Constraint { + static AttributeMapping = { + "value": 0, + } + resolveProposal(proposal, prefix) { + let {args} = this.resolve(prefix); + return [!(args[0] === true)]; + } + + test(prefix) { + let {args, returns} = this.resolve(prefix); + return !(args[0] === true) === returns[0]; + } + + getProposal(tripleIndex, proposed, prefix) { + let proposal = this.proposalObject; + proposal.providing = proposed; + proposal.cardinality = 1; + return proposal; + } +} + +providers.provide(">", GreaterThan); +providers.provide("<", LessThan); +providers.provide("<=", LessThanEqualTo); +providers.provide(">=", GreaterThanEqualTo); +providers.provide("!=", NotEqual); +providers.provide("=", Equal); +providers.provide("and", And); +providers.provide("or", Or); +providers.provide("toggle", Toggle); diff --git a/src/runtime/providers/math.ts b/src/runtime/providers/math.ts new file mode 100644 index 000000000..1c7329323 --- /dev/null +++ b/src/runtime/providers/math.ts @@ -0,0 +1,476 @@ +//--------------------------------------------------------------------- +// Math providers +//--------------------------------------------------------------------- + +import {Constraint} from "../join"; +import * as providers from "./index"; +import {deprecated} from "../util/deprecated"; + +abstract class TotalFunctionConstraint extends Constraint { + abstract getReturnValue(args: any[]) : number; + + // Proposes the return value of the total function as the value for the + // proposed variable. + resolveProposal(proposal, prefix) { + let {args} = this.resolve(prefix); + return [this.getReturnValue(args)]; + } + + // Check if our return is equivalent to the result of the total function. + test(prefix) { + let {args, returns} = this.resolve(prefix); + return this.getReturnValue(args) === returns[0]; + } + + // Total functions always have a cardinality of 1 + getProposal(tripleIndex, proposed, prefix) { + let proposal = this.proposalObject; + proposal.providing = proposed; + proposal.cardinality = 1; + return proposal; + } +} + +abstract class TrigConstraint extends TotalFunctionConstraint{ + static AttributeMapping = { + "degrees": 0, + "radians": 1 + } + + resolveTrigAttributes(args) : any { + let degrees = args[0]; + let radians = args[1]; + + //degrees which overrides radians. + if (! isNaN(degrees)){ radians = degreesToRadians(degrees);} + return radians; + } +} + +abstract class ValueOnlyConstraint extends TotalFunctionConstraint{ + static AttributeMapping = { + "value": 0 + } +} + +function radiansToDegrees(radians:number){ + return radians * (180 / Math.PI); +} + + +function degreesToRadians(degrees:number){ + return degrees * (Math.PI / 180); +} + +class Add extends TotalFunctionConstraint { + getReturnValue(args) { + return args[0] + args[1]; + } +} + +class Subtract extends TotalFunctionConstraint { + getReturnValue(args) { + return args[0] - args[1]; + } +} + +class Multiply extends TotalFunctionConstraint { + getReturnValue(args) { + return args[0] * args[1]; + } +} + +class Divide extends TotalFunctionConstraint { + getReturnValue(args) { + return args[0] / args[1]; + } +} + +class Sin extends TrigConstraint { + getReturnValue(args) { + return Math.sin(this.resolveTrigAttributes(args)); + } +} + +class Cos extends TrigConstraint { + getReturnValue(args) { + return Math.cos(this.resolveTrigAttributes(args)); + } +} + +class Tan extends TrigConstraint { + getReturnValue(args) { + return Math.tan(this.resolveTrigAttributes(args)); + } +} + +class ASin extends ValueOnlyConstraint { + getReturnValue(args) { + return Math.asin(args[0]); + } +} + +class ACos extends ValueOnlyConstraint { + getReturnValue(args) { + return Math.acos(args[0]); + } +} + +class ATan extends ValueOnlyConstraint { + getReturnValue(args) { + return Math.atan(args[0]); + } +} + +class ATan2 extends TotalFunctionConstraint { + static AttributeMapping = { + "x": 0, + "y": 1 + } + getReturnValue(args) { + return (Math.atan2(args[0] ,args[1])); + } +} + +//Hyperbolic Functions +class SinH extends ValueOnlyConstraint { + sinh (x: number):number{ + var y = Math.exp(x); + return (y - 1 / y) / 2; + } + getReturnValue(args) { + return (this.sinh(args[0])); + } +} + +class CosH extends ValueOnlyConstraint { + cosh (x: number):number{ + var y = Math.exp(x); + return (y + 1 / y) / 2; + } + getReturnValue(args) { + return (this.cosh(args[0])); + } +} + +class TanH extends ValueOnlyConstraint { + tanh(x : number) : number { + if (x === Infinity) { + return 1; + } else if (x === -Infinity) { + return -1; + } else { + let y = Math.exp(2 * x); + return (y - 1) / (y + 1); + } + } + getReturnValue(args) { + return (this.tanh(args[0])); + } +} + +//Inverse Hyperbolic +class ASinH extends ValueOnlyConstraint { + asinh (x: number):number{ + if (x === -Infinity) { + return x; + } else { + return Math.log(x + Math.sqrt(x * x + 1)); + } + } + getReturnValue(args) { + return this.asinh(args[0]); + } +} + +class ACosH extends ValueOnlyConstraint { + acosh (x: number):number{ + //How do we handle number outside of range in Eve? + if (x < 1) {return NaN} + return Math.log(x + Math.sqrt(x * x - 1)); + } + + getReturnValue(args) { + return this.acosh(args[0]); + } +} + +class ATanH extends ValueOnlyConstraint { + atanh(x : number) : number { + //How do we handle number outside of range in Eve? + if (Math.abs(x) > 1) {return NaN} + return Math.log((1 + x) / (1 - x)) / 2; + } + + getReturnValue(args) { + return this.atanh(args[0]); + } +} + +class Log extends TotalFunctionConstraint { + static AttributeMapping = { + "value": 0, + "base" : 1 + } + + getReturnValue(args) { + let baselog = 1; + if (! (isNaN(args[1]))){ + baselog = Math.log(args[1]); + } + return (Math.log(args[0]) / baselog); + } +} + +class Exp extends ValueOnlyConstraint { + getReturnValue(args) { + return (Math.exp(args[0])); + } +} + +class Pow extends TotalFunctionConstraint { + static AttributeMapping = { + "value": 0, + "by": 1, + } + + getReturnValue(args) { + return Math.pow(args[0], args[1]); + } +} + +class Mod extends TotalFunctionConstraint { + static AttributeMapping = { + "value": 0, + "by": 1, + } + + getReturnValue(args) { + return args[0] % args[1]; + } +} + +class Abs extends ValueOnlyConstraint { + getReturnValue(args) { + return Math.abs(args[0]); + } +} + +class Floor extends ValueOnlyConstraint { + getReturnValue(args) { + return Math.floor(args[0]); + } +} + +class Ceiling extends ValueOnlyConstraint { + getReturnValue(args) { + return Math.ceil(args[0]); + } +} + +class Random extends TotalFunctionConstraint { + static AttributeMapping = { + "seed": 0, + } + + static cache = {}; + + getReturnValue(args) { + let [seed] = args; + let found = Random.cache[seed]; + if(found) return found; + return Random.cache[seed] = Math.random(); + } +} + +class Gaussian extends TotalFunctionConstraint { + static AttributeMapping = { + "seed": 0, + "σ": 1, + "μ": 2 + } + + static cache = {}; + + getReturnValue(args) { + let [seed, sigma, mu] = args; + if (sigma === undefined) sigma = 1.0 + if (mu === undefined) mu = 0.0 + let found = Gaussian.cache[seed]; + if(found) return found; + let u1 = Math.random() + let u2 = Math.random() + let z0 = Math.sqrt(-2.0 * Math.log(u1) ) * Math.cos (Math.PI * 2 * u2) + let key = "" + seed + sigma + mu + let res = z0 * sigma + mu; + Gaussian.cache[key] = res + return res + } +} + +class Round extends ValueOnlyConstraint { + getReturnValue(args) { + return Math.round(args[0]); + } +} + +class ToFixed extends TotalFunctionConstraint { + static AttributeMapping = { + "value": 0, + "places": 1, + } + + getReturnValue(args) { + return args[0].toFixed(args[1]); + } +} + +class Range extends Constraint { + static AttributeMapping = { + "from": 0, + "to": 1, + "increment": 2, + } + + resolveProposal(proposal, prefix) { + let {args} = this.resolve(prefix); + let [from, to, increment] = args; + increment = increment || 1; + let results = []; + if(from <= to) { + for (let val = from; val <= to; val += increment) { + results.push(val); + } + } else { + for (let val = from; val >= to; val += increment) { + results.push(val); + } + } + return results; + } + + test(prefix) { + let {args, returns} = this.resolve(prefix); + let [from, to, increment] = args; + increment = increment || 1; + let val = returns[0]; + let member = from <= val && val <= to && + ((val - from) % increment) == 0 + return member; + } + + getProposal(tripleIndex, proposed, prefix) { + let {args} = this.resolve(prefix); + let [from, to, increment] = args; + increment = args[2] || 1; + let proposal = this.proposalObject; + proposal.providing = proposed; + if(from <= to && increment < 0) { + proposal.cardinality = 0; + return proposal; + } else if(from > to && increment > 0) { + proposal.cardinality = 0; + return proposal; + } + proposal.cardinality = Math.ceil(Math.abs((to - from + 1) / increment)); + return proposal; + } +} + +//Constants +class PI extends TotalFunctionConstraint { + getReturnValue(args) { + return Math.PI; + } +} + +class E extends TotalFunctionConstraint { + getReturnValue(args) { + return Math.E; + } +} + +class LN2 extends TotalFunctionConstraint { + getReturnValue(args) { + return Math.LN2; + } +} + +class LN10 extends TotalFunctionConstraint { + getReturnValue(args) { + return Math.LN10; + } +} + +class LOG2E extends TotalFunctionConstraint { + getReturnValue(args) { + return Math.LOG2E; + } +} + +class LOG10E extends TotalFunctionConstraint { + getReturnValue(args) { + return Math.LOG10E; + } +} + +class SQRT1_2 extends TotalFunctionConstraint { + getReturnValue(args) { + return Math.SQRT1_2; + } +} + +class SQRT2 extends TotalFunctionConstraint { + getReturnValue(args) { + return Math.SQRT2; + } +} + +providers.provide("+", Add); +providers.provide("-", Subtract); +providers.provide("*", Multiply); +providers.provide("/", Divide); + +providers.provide("log", Log); +providers.provide("exp", Exp); + +//Trig and Inverse Trig +providers.provide("sin", Sin); +providers.provide("cos", Cos); +providers.provide("tan", Tan); + +providers.provide("asin", ASin); +providers.provide("acos", ACos); +providers.provide("atan", ATan); + +providers.provide("atan2", ATan2); + +//Hyperbolic Functions. +providers.provide("sinh", SinH); +providers.provide("cosh", CosH); +providers.provide("tanh", TanH); +providers.provide("asinh", ASinH); +providers.provide("acosh", ACosH); +providers.provide("atanh", ATanH); + +providers.provide("floor", Floor); +providers.provide("ceiling", Ceiling); + +providers.provide("abs", Abs); +providers.provide("mod", Mod); +providers.provide("pow", Pow); +providers.provide("random", Random); +providers.provide("range", Range); +providers.provide("round", Round); +providers.provide("gaussian", Gaussian); +providers.provide("to-fixed", ToFixed); + +//Constants +providers.provide("pi", PI); +providers.provide("e", E); +providers.provide("ln2", LN2); +providers.provide("ln10", LN10); +providers.provide("log2e",LOG2E ); +providers.provide("log10e",LOG10E ); +providers.provide("sqrt1/2", SQRT1_2); +providers.provide("sqrt2", SQRT2); diff --git a/src/runtime/providers/sort.ts b/src/runtime/providers/sort.ts new file mode 100644 index 000000000..e3864eaf0 --- /dev/null +++ b/src/runtime/providers/sort.ts @@ -0,0 +1,164 @@ +//--------------------------------------------------------------------- +// Sort provider +//--------------------------------------------------------------------- + +import {Constraint, isVariable, resolve, toValue} from "../join"; +import * as providers from "./index"; + +export class Sort extends Constraint { + static isAggregate = true; + static AttributeMapping = { + "value": 0, + "direction": 1, + "per": 2, + } + + valueVars: any[]; + directionVars: any[]; + groupVars: any[]; + resolvedGroup: any[]; + resolvedValue: any[]; + resolvedDirection: any[]; + resolvedAggregate: {group: any[], value: any[], direction: any}; + aggregateResults: any; + + constructor(id: string, args: any[], returns: any[]) { + super(id, args, returns); + let [value, direction, per] = args; + if(value === undefined) { + this.valueVars = []; + } else if(isVariable(value)) { + this.valueVars = [value]; + } else { + this.valueVars = value; + } + if(direction === undefined) { + this.directionVars = []; + } else if(direction.constructor === Array) { + this.directionVars = direction; + } else { + this.directionVars = [direction]; + } + if(per === undefined) { + this.groupVars = []; + } else if(isVariable(per)) { + this.groupVars = [per]; + } else { + this.groupVars = per; + } + this.resolvedGroup = []; + this.resolvedValue = []; + this.resolvedDirection = []; + this.resolvedAggregate = {group: this.resolvedGroup, value: this.resolvedValue, direction: this.resolvedDirection}; + this.aggregateResults = {}; + } + + resolveAggregate(prefix) { + resolve(this.valueVars, prefix, this.resolvedValue) + resolve(this.directionVars, prefix, this.resolvedDirection) + resolve(this.groupVars, prefix, this.resolvedGroup) + let resolved = this.resolvedAggregate; + return resolved; + } + + aggregate(rows: any[]) { + let groupKeys = []; + let groups = {}; + for(let row of rows) { + let {group, value, direction} = this.resolveAggregate(row); + let groupKey = "[]"; + if(group.length !== 0) { + groupKey = JSON.stringify(group); + } + let groupValues = groups[groupKey]; + if(groupValues === undefined) { + groupKeys.push(groupKey); + groupValues = groups[groupKey] = {}; + } + let valueKey = JSON.stringify(value); + if(groupValues[valueKey] === undefined) { + groupValues[valueKey] = true; + groupValues["direction"] = direction.slice(); + this.adjustAggregate(groupValues, value, valueKey); + } + } + for(let key of groupKeys) { + this.finalizeGroup(groups[key]); + } + this.aggregateResults = groups; + return groups; + } + + resolveProposal(proposal, prefix) { + if(proposal.index) { + let {value} = this.resolveAggregate(prefix); + return [proposal.index[JSON.stringify(value)]]; + } + return []; + } + + test(prefix) { + let {group} = this.resolveAggregate(prefix); + let resultGroup = this.aggregateResults[JSON.stringify(group)]; + if(resultGroup !== undefined) { + let returns = resolve(this.returns, prefix, this.resolvedReturns); + return returns[0] === resultGroup.result; + } + } + + getProposal(multiIndex, proposed, prefix) { + let {group} = this.resolveAggregate(prefix); + let resultGroup = this.aggregateResults[JSON.stringify(group)]; + let proposal = this.proposalObject; + if(resultGroup) { + proposal.index = resultGroup + proposal.providing = proposed; + proposal.cardinality = 1; + } else { + proposal.index = undefined; + proposal.providing = proposed; + proposal.cardinality = 0; + } + return proposal; + } + + finalizeGroup(group) { + let result = group.result; + let direction = group.direction; + let multi = 1; + result.sort((a, b) => { + let ix = -1; + for(let aItem of a) { + ix++; + if(direction[ix] !== undefined) { + if(direction[ix] === "down") { + multi = -1; + } else { + multi = 1; + } + } + if(aItem === b[ix]) continue; + if(aItem > b[ix]) { + return 1 * multi; + } else { + return -1 * multi; + } + } + return 0; + }) + let ix = 1; + for(let item of result) { + group[JSON.stringify(item)] = ix; + ix++; + } + } + + adjustAggregate(group, value, key) { + if(!group.result) { + group.result = []; + } + group.result.push(value.slice()); + } +} + +providers.provide("sort", Sort); diff --git a/src/runtime/providers/string.ts b/src/runtime/providers/string.ts new file mode 100644 index 000000000..6b3d0316a --- /dev/null +++ b/src/runtime/providers/string.ts @@ -0,0 +1,259 @@ +//--------------------------------------------------------------------- +// String providers +//--------------------------------------------------------------------- + +import {Constraint} from "../join"; +import * as providers from "./index"; + +// Concat strings together. Args expects a set of variables/string constants +// to concatenate together and an array with a single return variable +class Concat extends Constraint { + // To resolve a proposal, we concatenate our resolved args + resolveProposal(proposal, prefix) { + let {args} = this.resolve(prefix); + return [args.join("")]; + } + + // We accept a prefix if the return is equivalent to concatentating + // all the args + test(prefix) { + let {args, returns} = this.resolve(prefix); + return args.join("") === returns[0]; + } + + // concat always returns cardinality 1 + getProposal(tripleIndex, proposed, prefix) { + let proposal = this.proposalObject; + proposal.providing = proposed; + proposal.cardinality = 1; + return proposal; + } +} + + + +class Split extends Constraint { + static AttributeMapping = { + "text": 0, + "by": 1, + } + static ReturnMapping = { + "token": 0, + "index": 1, + } + + returnType: "both" | "index" | "token"; + + constructor(id: string, args: any[], returns: any[]) { + super(id, args, returns); + if(this.returns[1] !== undefined && this.returns[0] !== undefined) { + this.returnType = "both" + } else if(this.returns[1] !== undefined) { + this.returnType = "index"; + } else { + this.returnType = "token"; + } + } + + resolveProposal(proposal, prefix) { + let {returns} = this.resolve(prefix); + let tokens = proposal.index; + let results = tokens; + if(this.returnType === "both") { + results = []; + let ix = 1; + for(let token of tokens) { + results.push([token, ix]); + ix++; + } + } else if(this.returnType === "index") { + results = []; + let ix = 1; + for(let token of tokens) { + results.push(ix); + ix++; + } + } + return results; + } + + test(prefix) { + let {args, returns} = this.resolve(prefix); + // @TODO: this is expensive, we should probably try to cache the split somehow + return args[0].split(args[1])[returns[1]] === returns[0]; + } + + getProposal(tripleIndex, proposed, prefix) { + let {args} = this.resolve(prefix); + let proposal = this.proposalObject; + if(this.returnType === "both") { + proposal.providing = [this.returns[0], this.returns[1]]; + } else if(this.returnType == "index") { + proposal.providing = this.returns[1]; + } else { + proposal.providing = this.returns[0]; + } + proposal.index = args[0].split(args[1]); + proposal.cardinality = proposal.index.length; + return proposal; + } +} + + +// substring over the field 'text', with the base index being 1, inclusive, 'from' defaulting +// to the beginning of the string, and 'to' the end +class Substring extends Constraint { + static AttributeMapping = { + "text": 0, + "from": 1, + "to": 2, + } + static ReturnMapping = { + "value": 0, + } + // To resolve a proposal, we concatenate our resolved args + resolveProposal(proposal, prefix) { + let {args, returns} = this.resolve(prefix); + let from = 0; + let text = args[0]; + let to = text.length; + if (args[1] != undefined) from = args[1] - 1; + if (args[2] != undefined) to = args[2]; + return [text.substring(from, to)]; + } + + test(prefix) { + let {args, returns} = this.resolve(prefix); + let from = 0; + let text = args[0]; + if(typeof text !== "string") return false; + let to = text.length; + if (args[1] != undefined) from = args[1] - 1; + if (args[2] != undefined) to = args[2]; + console.log("test string", text.substring(from, to), from, to, returns[0]); + return text.substring(from, to) === returns[0]; + } + + // substring always returns cardinality 1 + getProposal(tripleIndex, proposed, prefix) { + let proposal = this.proposalObject; + let {args} = this.resolve(prefix); + if(typeof args[0] !== "string") { + proposal.cardinality = 0; + } else { + proposal.providing = proposed; + proposal.cardinality = 1; + } + return proposal; + } +} + +class Convert extends Constraint { + static AttributeMapping = { + "value": 0, + "to": 1, + } + static ReturnMapping = { + "converted": 0, + } + + resolveProposal(proposal, prefix) { + let {args, returns} = this.resolve(prefix); + let from = 0; + let value = args[0]; + let to = args[1]; + let converted; + if(to === "number") { + converted = +value; + if(isNaN(converted)) throw new Error("Unable to deal with NaN in the proposal stage."); + } else if(to === "string") { + converted = ""+value; + } + return [converted]; + } + + test(prefix) { + let {args, returns} = this.resolve(prefix); + let value = args[0]; + let to = args[1]; + + let converted; + if(to === "number") { + converted = +value; + if(isNaN(converted)) return false; + if(converted === "") return false; + return + } else if(to === "string") { + converted = ""+value; + } else { + return false; + } + + return converted === returns[0]; + } + + // 1 if valid, 0 otherwise + getProposal(tripleIndex, proposed, prefix) { + let proposal = this.proposalObject; + let {args} = this.resolve(prefix); + let value = args[0]; + let to = args[1]; + + proposal.cardinality = 1; + proposal.providing = proposed; + + if(to === "number") { + if(isNaN(+value) || value === "") proposal.cardinality = 0; + } else if(to === "string") { + } else { + proposal.cardinality = 0; + } + + return proposal; + } +} + +// Urlencode a string +class Urlencode extends Constraint { + static AttributeMapping = { + "text": 0 + } + static ReturnMapping = { + "value": 0, + } + + // To resolve a proposal, we urlencode a text + resolveProposal(proposal, prefix) { + let {args, returns} = this.resolve(prefix); + let value = args[0]; + let converted; + converted = encodeURIComponent(value); + return [converted]; + } + + test(prefix) { + let {args, returns} = this.resolve(prefix); + let value = args[0]; + + let converted = encodeURIComponent(value); + + return converted === returns[0]; + } + + // Urlencode always returns cardinality 1 + getProposal(tripleIndex, proposed, prefix) { + let proposal = this.proposalObject; + let {args} = this.resolve(prefix); + let value = args[0]; + proposal.cardinality = 1; + proposal.providing = proposed; + return proposal; + } +} + + +providers.provide("concat", Concat); +providers.provide("split", Split); +providers.provide("substring", Substring); +providers.provide("convert", Convert); +providers.provide("urlencode", Urlencode); \ No newline at end of file diff --git a/src/runtime/runtime.ts b/src/runtime/runtime.ts new file mode 100644 index 000000000..2e03e80b4 --- /dev/null +++ b/src/runtime/runtime.ts @@ -0,0 +1,288 @@ +//--------------------------------------------------------------------- +// Runtime +//--------------------------------------------------------------------- + +import {PerformanceTracker, NoopPerformanceTracker} from "./performance"; + +const TRACK_PERFORMANCE = true; +const MAX_ROUNDS = 30; + +//--------------------------------------------------------------------- +// Setups +//--------------------------------------------------------------------- + +import {} from "./join" +import {MultiIndex, TripleIndex} from "./indexes" +import {Block} from "./block" +import {Changes} from "./changes" +import {Action} from "./actions" +import {ids} from "./id"; + +//--------------------------------------------------------------------- +// Database +//--------------------------------------------------------------------- + +export class Database { + static id = 1; + + id: string; + blocks: Block[]; + index: TripleIndex; + evaluations: Evaluation[]; + nonExecuting: boolean; + + constructor() { + this.id = `db|${Database.id}`; + Database.id++; + this.evaluations = []; + this.blocks = []; + this.index = new TripleIndex(0); + } + + register(evaluation: Evaluation) { + if(this.evaluations.indexOf(evaluation) === -1) { + this.evaluations.push(evaluation); + } + } + + unregister(evaluation: Evaluation) { + let evals = this.evaluations; + let index = evals.indexOf(evaluation); + if(index > -1) { + evals.splice(index, 1); + } else { + throw new Error("Trying to unregister an evaluation that isn't registered with this database"); + } + } + + onFixpoint(currentEvaluation: Evaluation, changes: Changes) { + let name = currentEvaluation.databaseToName(this); + let commit = changes.toCommitted({[name]: true}); + if(commit.length === 0) return; + for(let evaluation of this.evaluations) { + if(evaluation !== currentEvaluation) { + evaluation.queue(commit); + } + } + } + + toTriples() { + return this.index.toTriples(true); + } + + analyze(e: Evaluation, d: Database) {} +} + +//--------------------------------------------------------------------- +// Evaluation +//--------------------------------------------------------------------- + +export class Evaluation { + queued: boolean; + commitQueue: any[]; + multiIndex: MultiIndex; + databases: Database[]; + errorReporter: any; + databaseNames: {[dbId: string]: string}; + nameToDatabase: {[name: string]: Database}; + perf: PerformanceTracker; + + constructor(index?) { + this.queued = false; + this.commitQueue = []; + this.databases = []; + this.databaseNames = {}; + this.nameToDatabase = {}; + this.multiIndex = index || new MultiIndex(); + if(TRACK_PERFORMANCE) { + this.perf = new PerformanceTracker(); + } else { + this.perf = new NoopPerformanceTracker(); + } + } + + error(kind: string, error: string) { + if(this.errorReporter) { + this.errorReporter(kind, error); + } else { + console.error(kind + ":", error); + } + } + + unregisterDatabase(name) { + let db = this.nameToDatabase[name]; + delete this.nameToDatabase[name]; + if(!db) return; + + this.databases.splice(this.databases.indexOf(db), 1); + delete this.databaseNames[db.id]; + this.multiIndex.unregister(name); + db.unregister(this); + } + + registerDatabase(name: string, db: Database) { + if(this.nameToDatabase[name]) { + throw new Error("Trying to register a database name that is already registered: " + name); + } + for(let database of this.databases) { + db.analyze(this, database); + database.analyze(this, db); + } + this.databases.push(db); + this.databaseNames[db.id] = name; + this.nameToDatabase[name] = db; + this.multiIndex.register(name, db.index); + db.register(this); + } + + databaseToName(db: Database) { + return this.databaseNames[db.id]; + } + + getDatabase(name: string) { + return this.nameToDatabase[name]; + } + + blocksFromCommit(commit) { + let perf = this.perf; + let start = perf.time(); + let blocks = []; + let index = this.multiIndex; + let tagsCache = {}; + for(let database of this.databases) { + if(database.nonExecuting) continue; + for(let block of database.blocks) { + if(block.dormant) continue; + let checker = block.checker; + for(let ix = 0, len = commit.length; ix < len; ix += 6) { + let change = commit[ix]; + let e = commit[ix + 1]; + let a = commit[ix + 2]; + let v = commit[ix + 3]; + + let tags = tagsCache[e]; + if(tags === undefined) { + tags = tagsCache[e] = index.dangerousMergeLookup(e,"tag",undefined); + } + + if(checker.check(index, change, tags, e, a, v)) { + blocks.push(block); + break; + } + } + } + } + perf.blockCheck(start); + // console.log("executing blocks", blocks.map((x) => x)); + return blocks; + } + + + getAllBlocks() { + let blocks = []; + for(let database of this.databases) { + if(database.nonExecuting) continue; + for(let block of database.blocks) { + if(block.dormant) continue; + blocks.push(block); + } + } + return blocks; + } + + queue(commit) { + if(!commit.length) return; + if(!this.queued) { + let self = this; + process.nextTick(() => { + let commits = []; + for(let queued of self.commitQueue) { + for(let field of queued) { + commits.push(field); + } + } + this.fixpoint(new Changes(this.multiIndex), this.blocksFromCommit(commits)); + }); + } + this.commitQueue.push(commit); + } + + createChanges() { + return new Changes(this.multiIndex); + } + + executeActions(actions: Action[], changes = this.createChanges()) { + for(let action of actions) { + action.execute(this.multiIndex, [], changes); + } + let committed = changes.commit(); + return this.fixpoint(changes, this.blocksFromCommit(committed)); + } + + fixpoint(changes = new Changes(this.multiIndex), blocks = this.getAllBlocks()) { + let perf = this.perf; + let start = perf.time(); + let commit; + changes.changed = true; + while(changes.changed && changes.round < MAX_ROUNDS) { + changes.nextRound(); + // console.groupCollapsed("Round" + changes.round); + for(let block of blocks) { + let start = perf.time(); + block.execute(this.multiIndex, changes); + perf.block(block.id, start); + } + // console.log(changes); + commit = changes.commit(); + blocks = this.blocksFromCommit(commit); + // console.groupEnd(); + } + if(changes.round >= MAX_ROUNDS) { + this.error("Fixpoint Error", "Evaluation failed to fixpoint"); + } + perf.fixpoint(start); + // console.log("TOTAL ROUNDS", changes.round, perf.time(start)); + // console.log(changes); + for(let database of this.databases) { + database.onFixpoint(this, changes); + } + return changes; + } + + save() { + let results = {}; + for(let database of this.databases) { + let name = this.databaseToName(database); + let values = database.toTriples(); + for(let value of values) { + let [e,a,v,n] = value; + if(ids.isId(e)) value[0] = ids.parts(e); + if(ids.isId(v)) value[2] = ids.parts(v); + } + results[name] = values; + } + return results; + } + + load(dbs: Object) { + let changes = this.createChanges(); + for(let databaseName of Object.keys(dbs)) { + let facts = dbs[databaseName]; + let db = this.getDatabase(databaseName); + let index = db.index; + for(let fact of facts) { + let [e,a,v,n] = fact; + if(ids.isId(e)) e = ids.load(e); + if(ids.isId(v)) v = ids.load(v); + changes.store(databaseName,e,a,v,n); + } + } + this.executeActions([], changes); + } + + close() { + for(let database of this.databases) { + database.unregister(this); + } + } +} diff --git a/src/runtime/runtimeClient.ts b/src/runtime/runtimeClient.ts new file mode 100644 index 000000000..0e03719dd --- /dev/null +++ b/src/runtime/runtimeClient.ts @@ -0,0 +1,253 @@ +//--------------------------------------------------------------------- +// RuntimeClient +//--------------------------------------------------------------------- + +import {Evaluation, Database} from "./runtime"; +import * as join from "./join"; +import * as parser from "./parser"; +import * as builder from "./builder"; +import {ActionImplementations} from "./actions"; +import {BrowserSessionDatabase, BrowserEventDatabase, BrowserViewDatabase, BrowserEditorDatabase, BrowserInspectorDatabase} from "./databases/browserSession"; +import * as system from "./databases/system"; +import * as analyzer from "./analyzer"; +import {ids} from "./id"; + +//--------------------------------------------------------------------- +// Responder +//--------------------------------------------------------------------- + +export abstract class RuntimeClient { + lastParse: any; + evaluation: Evaluation; + extraDBs: any; + + constructor(extraDBs:any = {}) { + this.extraDBs = extraDBs; + } + + abstract send(json): void; + + load(code:string, context:string) { + code = code || ""; + let {results, errors} : {results: any, errors: any[]} = parser.parseDoc(code, context); + if(errors && errors.length) console.error(errors); + results.code = code; + this.lastParse = results; + this.makeEvaluation(); + this.evaluation.fixpoint(); + } + + makeEvaluation() { + if(this.evaluation) { + this.evaluation.close(); + this.evaluation = undefined; + } + let parse = this.lastParse; + let build = builder.buildDoc(parse); + let {blocks, errors} = build; + this.sendErrors(errors); + // TODO: What is the right way to gate analysis? This seems hacky, but I'm not sure + // that the RuntimeClient should really know/care about whether or not the editor is + // hooked up. Maybe there should be a flag for analysis instead? + if(this.extraDBs["editor"]) { + analyzer.analyze(blocks.map((block) => block.parse), parse.spans, parse.extraInfo); + } + + let ev = new Evaluation(); + let session = new Database(); + session.blocks = blocks; + ev.registerDatabase("session", session); + + let extraDBs = this.extraDBs; + if(!extraDBs["browser"]) { + ev.registerDatabase("browser", new BrowserSessionDatabase(this)); + } + if(!extraDBs["event"]) { + ev.registerDatabase("event", new BrowserEventDatabase()); + } + + if(!extraDBs["system"]) { + ev.registerDatabase("system", system.instance); + } + + for(let dbName of Object.keys(this.extraDBs)) { + let db = extraDBs[dbName]; + ev.registerDatabase(dbName, db); + } + + ev.errorReporter = (kind, error) => { + this.send(JSON.stringify({type: "error", kind, message: error})); + } + + this.evaluation = ev; + return ev; + } + + sendErrors(errors) { + if(!errors.length) return; + let spans = []; + let extraInfo = {}; + for(let error of errors) { + error.injectSpan(spans, extraInfo); + } + this.send(JSON.stringify({type: "comments", spans, extraInfo})) + return true; + } + + handleEvent(json:string) { + let data = JSON.parse(json); + + // Events are expected to be objects that have a type property + // if they aren't, we toss the event out + if(typeof data !== "object" || data.type === undefined) { + console.error("Got invalid JSON event: " + json); + return; + } + + if(data.type === "event") { + if(!this.evaluation) return; + // console.info("EVENT", json); + let scopes = ["event"]; + let actions = []; + for(let insert of data.insert) { + let [e, a, v] = insert; + // @TODO: this is a hack to deal with external ids. We should really generate + // a local id for them + if(e[0] === "⍦") e = ids.get([e]); + if(v[0] === "⍦") v = ids.get([v]); + actions.push(new ActionImplementations["+="]("event", e, a, v, "event", scopes)); + } + this.evaluation.executeActions(actions); + } else if(data.type === "close") { + if(!this.evaluation) return; + this.evaluation.close(); + this.evaluation = undefined; + } else if(data.type === "parse") { + let {results, errors}: {results: any, errors: any[]} = parser.parseDoc(data.code || "", "user"); + let {text, spans, extraInfo} = results; + let build = builder.buildDoc(results); + let {blocks, errors: buildErrors} = build; + results.code = data.code; + this.lastParse = results; + for(let error of buildErrors) { + error.injectSpan(spans, extraInfo); + } + this.send(JSON.stringify({type: "parse", generation: data.generation, text, spans, extraInfo})); + } else if(data.type === "eval") { + if(this.evaluation !== undefined && data.persist) { + let changes = this.evaluation.createChanges(); + let session = this.evaluation.getDatabase("session"); + for(let block of session.blocks) { + if(block.bindActions.length) { + block.updateBinds({positions: {}, info: []}, changes); + } + } + let build = builder.buildDoc(this.lastParse); + let {blocks, errors} = build; + let spans = []; + let extraInfo = {}; + if(this.extraDBs["editor"]) { + analyzer.analyze(blocks.map((block) => block.parse), spans, extraInfo); + } + this.sendErrors(errors); + for(let block of blocks) { + if(block.singleRun) block.dormant = true; + } + session.blocks = blocks; + this.evaluation.unregisterDatabase("session"); + this.evaluation.registerDatabase("session", session); + changes.commit(); + this.evaluation.fixpoint(changes); + } else { + let spans = []; + let extraInfo = {}; + this.makeEvaluation(); + this.evaluation.fixpoint(); + } + } else if(data.type === "tokenInfo") { + let spans = []; + let extraInfo = {}; + analyzer.tokenInfo(this.evaluation, data.tokenId, spans, extraInfo) + this.send(JSON.stringify({type: "comments", spans, extraInfo})) + } else if(data.type === "findNode") { + let {recordId, node} = data; + let spans = []; + let extraInfo = {}; + let spanId = analyzer.nodeIdToRecord(this.evaluation, data.node, spans, extraInfo); + this.send(JSON.stringify({type: "findNode", recordId, spanId})); + } else if(data.type === "findSource") { + let spans = []; + let extraInfo = {}; + let spanId = analyzer.findSource(this.evaluation, data, spans, extraInfo); + this.send(JSON.stringify(data)); + } else if(data.type === "findRelated") { + let spans = []; + let extraInfo = {}; + let spanId = analyzer.findRelated(this.evaluation, data, spans, extraInfo); + this.send(JSON.stringify(data)); + } else if(data.type === "findValue") { + let spans = []; + let extraInfo = {}; + let spanId = analyzer.findValue(this.evaluation, data, spans, extraInfo); + this.send(JSON.stringify(data)); + } else if(data.type === "findCardinality") { + let spans = []; + let extraInfo = {}; + let spanId = analyzer.findCardinality(this.evaluation, data, spans, extraInfo); + this.send(JSON.stringify(data)); + } else if(data.type === "findAffector") { + let spans = []; + let extraInfo = {}; + let spanId = analyzer.findAffector(this.evaluation, data, spans, extraInfo); + this.send(JSON.stringify(data)); + } else if(data.type === "findFailure") { + let spans = []; + let extraInfo = {}; + let spanId = analyzer.findFailure(this.evaluation, data, spans, extraInfo); + this.send(JSON.stringify(data)); + } else if(data.type === "findRootDrawers") { + let spans = []; + let extraInfo = {}; + let spanId = analyzer.findRootDrawers(this.evaluation, data, spans, extraInfo); + this.send(JSON.stringify(data)); + } else if(data.type === "findMaybeDrawers") { + let spans = []; + let extraInfo = {}; + let spanId = analyzer.findMaybeDrawers(this.evaluation, data, spans, extraInfo); + this.send(JSON.stringify(data)); + } else if(data.type === "findPerformance") { + let perf = this.evaluation.perf; + let userBlocks = {}; + for(let block of this.evaluation.getDatabase("session").blocks) { + userBlocks[block.id] = true; + } + let perfInfo = perf.asObject(userBlocks) as any; + perfInfo.type = "findPerformance"; + perfInfo.requestId = data.requestId; + this.send(JSON.stringify(perfInfo)); + } else if(data.type === "findRecordsFromToken") { + let spans = []; + let extraInfo = {}; + let spanId = analyzer.findRecordsFromToken(this.evaluation, data, spans, extraInfo); + this.send(JSON.stringify(data)); + } else if(data.type === "dumpState") { + let dbs = this.evaluation.save() as any; + let code = this.lastParse.code; + let output = JSON.stringify({code, databases: {"session": dbs.session}}); + this.send(JSON.stringify({type: "dumpState", state: output})); + } else if(data.type === "load") { + let spans = []; + let extraInfo = {}; + this.makeEvaluation(); + let blocks = this.evaluation.getDatabase("session").blocks; + for(let block of blocks) { + if(block.singleRun) { + block.dormant = true; + } + } + this.evaluation.load(data.info.databases); + } else { + console.error("Unhandled message type: " + json); + } + } +} diff --git a/src/runtime/server.ts b/src/runtime/server.ts new file mode 100644 index 000000000..b35fc3c94 --- /dev/null +++ b/src/runtime/server.ts @@ -0,0 +1,315 @@ +//--------------------------------------------------------------------- +// Server +//--------------------------------------------------------------------- + +import * as http from "http"; +import * as fs from "fs"; +import * as path from "path"; +import * as ws from "ws"; +import * as express from "express"; +import * as bodyParser from "body-parser"; +import * as minimist from "minimist"; + +import {config, Config, Owner} from "../config"; +import {ActionImplementations} from "./actions"; +import {PersistedDatabase} from "./databases/persisted"; +import {HttpDatabase} from "./databases/node/http"; +import {ServerDatabase} from "./databases/node/server"; +import {Database} from "./runtime"; +import {RuntimeClient} from "./runtimeClient"; +import {BrowserViewDatabase, BrowserEditorDatabase, BrowserInspectorDatabase} from "./databases/browserSession"; +import * as eveSource from "./eveSource"; + +//--------------------------------------------------------------------- +// Constants +//--------------------------------------------------------------------- + +const contentTypes = { + ".html": "text/html", + ".js": "application/javascript", + ".map": "application/javascript", + ".css": "text/css", + ".jpeg": "image/jpeg", + ".png": "image/png", +} + +const shared = new PersistedDatabase(); + + +global["browser"] = false; + +//--------------------------------------------------------------------- +// HTTPRuntimeClient +//--------------------------------------------------------------------- + +class HTTPRuntimeClient extends RuntimeClient { + server: ServerDatabase; + constructor() { + let server = new ServerDatabase(); + const dbs = { + "http": new HttpDatabase(), + "server": server, + "shared": shared, + "browser": new Database(), + } + super(dbs); + this.server = server; + } + + handle(request, response) { + this.server.handleHttpRequest(request, response); + } + + send(json) { + // there's nothing for this to do. + } +} + +//--------------------------------------------------------------------- +// Express app +//--------------------------------------------------------------------- + +function handleStatic(request, response) { + let url = request['_parsedUrl'].pathname; + let roots = [".", config.eveRoot]; + let completed = 0; + let results = {}; + for(let root of roots) { + let filepath = path.join(root, url); + fs.stat(filepath, (err, result) => { + completed += 1; + if(!err) results[root] = fs.readFileSync(filepath); + + if(completed === roots.length) { + for(let root of roots) { + if(results[root]) { + response.setHeader("Content-Type", `${contentTypes[path.extname(url)]}; charset=utf-8`); + response.end(results[root]); + return; + } + } + + return response.status(404).send("Looks like that asset is missing."); + } + }); + }; +} + +function createExpressApp() { + let filepath = config.path; + const app = express(); + + app.use(bodyParser.json()); + app.use(bodyParser.urlencoded({extended: true})); + + app.get("/build/workspaces.js", (request, response) => { + let packaged = eveSource.pack(); + response.setHeader("Content-Type", `application/javascript; charset=utf-8`); + response.end(packaged); + }); + + app.get("/assets/*", handleStatic); + app.get("/build/*", handleStatic); + app.get("/src/*", handleStatic); + app.get("/css/*", handleStatic); + app.get("/fonts/*", handleStatic); + + app.get("*", (request, response) => { + let client = new HTTPRuntimeClient(); + let content = ""; + if(filepath) content = fs.readFileSync(filepath).toString(); + client.load(content, "user"); + client.handle(request, response); + if(!client.server.handling) { + response.setHeader("Content-Type", `${contentTypes["html"]}; charset=utf-8`); + response.end(fs.readFileSync(path.join(config.eveRoot, "index.html"))); + } + }); + + app.post("*", (request, response) => { + let client = new HTTPRuntimeClient(); + let content = ""; + if(filepath) content = fs.readFileSync(filepath).toString(); + client.load(content, "user"); + client.handle(request, response); + if(!client.server.handling) { + return response.status(404).send("Looks like that asset is missing."); + } + }); + + return app; +} + +//--------------------------------------------------------------------- +// Websocket +//--------------------------------------------------------------------- + + + +class SocketRuntimeClient extends RuntimeClient { + socket: WebSocket; + + constructor(socket:WebSocket, withIDE:boolean) { + const dbs = { + "http": new HttpDatabase(), + "shared": shared, + } + if(withIDE) { + dbs["view"] = new BrowserViewDatabase(); + dbs["editor"] = new BrowserEditorDatabase(); + dbs["inspector"] = new BrowserInspectorDatabase(); + } + super(dbs); + this.socket = socket; + } + + send(json) { + if(this.socket && this.socket.readyState === 1) { + this.socket.send(json); + } + } +} + +function IDEMessageHandler(client:SocketRuntimeClient, message) { + let ws = client.socket; + let data = JSON.parse(message); + + if(data.type === "init") { + let {editor, runtimeOwner, controlOwner} = config; + let {url, hash} = data; + let path = hash !== "" ? hash : url; + + + let content = path && eveSource.find(path); + + if(!content && config.path) { + let workspace = config.internal ? "examples" : "root"; + // @FIXME: This hard-coding isn't technically wrong right now, but it's brittle and poor practice. + content = eveSource.get(config.path, workspace); + if(content) path = eveSource.getRelativePath(config.path, workspace); + } + + if(!content && config.internal) { + content = eveSource.get("quickstart.eve", "examples"); + if(content) path = eveSource.getRelativePath("quickstart.eve", "examples"); + } + + if(content) { + ws.send(JSON.stringify({type: "initProgram", runtimeOwner, controlOwner, path, code: content, withIDE: editor})); + if(runtimeOwner === Owner.server) { + client.load(content, "user"); + } + } else { + // @FIXME: Do we still need this fallback for anything? Cases where we need to run an eve file outside of a project? + fs.stat("." + path, (err, stats) => { + if(!err && stats.isFile()) { + let content = fs.readFileSync("." + path).toString(); + ws.send(JSON.stringify({type: "initProgram", runtimeOwner, controlOwner, path, code: content, withIDE: editor})); + } else { + ws.send(JSON.stringify({type: "initProgram", runtimeOwner, controlOwner, path, withIDE: editor})); + } + + if(runtimeOwner === Owner.server) { + client.load(content, "user"); + } + }); + } + } else if(data.type === "save"){ + eveSource.save(data.path, data.code); + } else if(data.type === "ping") { + // we don't need to do anything with pings, they're just to make sure hosts like + // Heroku don't shutdown our server. + } else { + client.handleEvent(message); + } +} + +function MessageHandler(client:SocketRuntimeClient, message) { + let ws = client.socket; + let data = JSON.parse(message); + if(data.type === "init") { + let {editor, runtimeOwner, controlOwner, path:filepath} = config; + // we do nothing here since the server is in charge of handling init. + let content = fs.readFileSync(filepath).toString(); + ws.send(JSON.stringify({type: "initProgram", runtimeOwner, controlOwner, path: filepath, code: content, withIDE: editor})); + if(runtimeOwner === Owner.server) { + client.load(content, "user"); + } + } else if(data.type === "event") { + client.handleEvent(message); + } else if(data.type === "ping") { + // we don't need to do anything with pings, they're just to make sure hosts like + // Heroku don't shutdown our server. + } else { + console.error("Invalid message sent: " + message); + } +} + +function initWebsocket(wss, withIDE:boolean) { + wss.on('connection', function connection(ws) { + let client = new SocketRuntimeClient(ws, withIDE); + let handler = withIDE ? IDEMessageHandler : MessageHandler; + if(!withIDE) { + // we need to initialize + } + ws.on('message', (message) => { + handler(client, message); + }) + ws.on("close", function() { + if(client.evaluation) { + client.evaluation.close(); + } + }); + }); +} + +//--------------------------------------------------------------------- +// Go! +//--------------------------------------------------------------------- + +export function run() { + // @FIXME: Split these out! + eveSource.add("eve", path.join(config.eveRoot, "examples")); + if(config.internal) { + eveSource.add("examples", path.join(config.eveRoot, "examples")); + } else { + eveSource.add("root", config.root); + } + + + // If a file was passed in, we need to make sure it actually exists + // now instead of waiting for the user to submit a request and then + // blowing up + if(config.path) { + try { + fs.statSync(config.path); + } catch(e) { + throw new Error("Can't load " + config.path); + } + } + + let app = createExpressApp(); + let server = http.createServer(app); + + let WebSocketServer = require('ws').Server; + let wss = new WebSocketServer({server}); + initWebsocket(wss, config.editor); + + server.listen(config.port, function(){ + console.log(`Eve is available at http://localhost:${config.port}. Point your browser there to access the Eve editor.`); + }); + + // If the port is already in use, display an error message + process.on('uncaughtException', function handleAddressInUse(err) { + if(err.errno === 'EADDRINUSE') { + console.log(`ERROR: Eve couldn't start because port ${config.port} is already in use.\n\nYou can select a different port for Eve using the "port" argument.\nFor example:\n\n> npm start -- --port 1234`); + } else { + throw err; + } + process.exit(1); + }); +} + +if(require.main === module) { + console.error("Please run eve using the installed eve binary."); +} diff --git a/src/runtime/util/deprecated.ts b/src/runtime/util/deprecated.ts new file mode 100644 index 000000000..652ff4a78 --- /dev/null +++ b/src/runtime/util/deprecated.ts @@ -0,0 +1,14 @@ +export function deprecated(message: string = 'Function {name} is deprecated.') { + return (instance, name, descriptor) => { + var original = descriptor.value; + var localMessage = message.replace('{name}', name); + + descriptor.value = function() { + console.warn(localMessage); + + return original.apply(instance, arguments); + }; + + return descriptor; + }; +} diff --git a/src/runtime/util/eavs.ts b/src/runtime/util/eavs.ts new file mode 100644 index 000000000..39c7933b7 --- /dev/null +++ b/src/runtime/util/eavs.ts @@ -0,0 +1,91 @@ +//--------------------------------------------------------------------- +// Utility functions for working with triples +//--------------------------------------------------------------------- + +import {Changes} from "../changes"; +import {TripleIndex} from "../indexes"; + +const JSON_NULL_ID = "external|json|null"; + +//--------------------------------------------------------------------- +// JS conversion +//--------------------------------------------------------------------- + +export function fromJS(changes: Changes, json: any, node: string, scope: string, idPrefix: string = "js") { + if(json === null || json === undefined) return JSON_NULL_ID; + if(json.constructor === Array) { + let arrayId = `${idPrefix}|array`; + changes.store(scope, arrayId, "tag", "array", node); + let ix = 0; + for(let value of json) { + ix++; + if(typeof value === "object") { + value = fromJS(changes, value, node, scope, `${arrayId}|${ix}`); + } + changes.store(scope, arrayId, ix, value, node); + } + return arrayId; + } else if(typeof json === "object") { + let objectId = `${idPrefix}|object`; + for(let key of Object.keys(json)) { + let value = json[key]; + if(value === null || value === undefined) { + changes.store(scope, JSON_NULL_ID, "tag", "json-null", node); + changes.store(scope, objectId, key, JSON_NULL_ID, node); + } else { + if(value.constructor === Array || typeof value === "object") { + value = fromJS(changes, value, node, scope, `${objectId}|${key}`); + } + changes.store(scope, objectId, key, value, node); + } + } + return objectId; + } else { + throw new Error("Trying to turn non-object/array JSON into EAVs." + JSON.stringify(json)); + } +} + +export function toJS(index: TripleIndex, recordId) { + let result; + if(recordId === JSON_NULL_ID) return null; + let isArray = index.lookup(recordId, "tag", "array"); + if(isArray !== undefined) { + result = []; + let ix = 1; + while(true) { + let valueIndex = index.lookup(recordId, ix); + if(valueIndex !== undefined) { + let curIndex = valueIndex.index; + for(let key of Object.keys(curIndex)) { + let value = curIndex[key].value; + if(index.lookup(value)) { + result[ix - 1] = toJS(index, value); + } else { + result[ix - 1] = value; + } + } + } else { + break; + } + ix++; + } + } else { + result = index.asObject(recordId); + for(let key of Object.keys(result)) { + let values = result[key]; + let valueIx = 0; + for(let value of values) { + if(index.lookup(value)) { + values[valueIx] = toJS(index, value); + } else { + values[valueIx] = value; + } + valueIx++; + } + if(values.length === 1) { + result[key] = values[0]; + } + } + } + return result; +} diff --git a/src/simplescrollbars.css b/src/simplescrollbars.css new file mode 100644 index 000000000..5eea7aa1b --- /dev/null +++ b/src/simplescrollbars.css @@ -0,0 +1,66 @@ +.CodeMirror-simplescroll-horizontal div, .CodeMirror-simplescroll-vertical div { + position: absolute; + background: #ccc; + -moz-box-sizing: border-box; + box-sizing: border-box; + border: 1px solid #bbb; + border-radius: 2px; +} + +.CodeMirror-simplescroll-horizontal, .CodeMirror-simplescroll-vertical { + position: absolute; + z-index: 6; + background: #eee; +} + +.CodeMirror-simplescroll-horizontal { + bottom: 0; left: 0; + height: 8px; +} +.CodeMirror-simplescroll-horizontal div { + bottom: 0; + height: 100%; +} + +.CodeMirror-simplescroll-vertical { + right: 0; top: 0; + width: 8px; +} +.CodeMirror-simplescroll-vertical div { + right: 0; + width: 100%; +} + + +.CodeMirror-overlayscroll .CodeMirror-scrollbar-filler, .CodeMirror-overlayscroll .CodeMirror-gutter-filler { + display: none; +} + +.CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div { + position: absolute; + background: #bcd; + border-radius: 3px; +} + +.CodeMirror-overlayscroll-horizontal, .CodeMirror-overlayscroll-vertical { + position: absolute; + z-index: 6; +} + +.CodeMirror-overlayscroll-horizontal { + bottom: 0; left: 0; + height: 6px; +} +.CodeMirror-overlayscroll-horizontal div { + bottom: 0; + height: 100%; +} + +.CodeMirror-overlayscroll-vertical { + right: 0; top: 0; + width: 6px; +} +.CodeMirror-overlayscroll-vertical div { + right: 0; + width: 100%; +} diff --git a/src/simplescrollbars.js b/src/simplescrollbars.js new file mode 100644 index 000000000..23f3e03f8 --- /dev/null +++ b/src/simplescrollbars.js @@ -0,0 +1,152 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + function Bar(cls, orientation, scroll) { + this.orientation = orientation; + this.scroll = scroll; + this.screen = this.total = this.size = 1; + this.pos = 0; + + this.node = document.createElement("div"); + this.node.className = cls + "-" + orientation; + this.inner = this.node.appendChild(document.createElement("div")); + + var self = this; + CodeMirror.on(this.inner, "mousedown", function(e) { + if (e.which != 1) return; + CodeMirror.e_preventDefault(e); + var axis = self.orientation == "horizontal" ? "pageX" : "pageY"; + var start = e[axis], startpos = self.pos; + function done() { + CodeMirror.off(document, "mousemove", move); + CodeMirror.off(document, "mouseup", done); + } + function move(e) { + if (e.which != 1) return done(); + self.moveTo(startpos + (e[axis] - start) * (self.total / self.size)); + } + CodeMirror.on(document, "mousemove", move); + CodeMirror.on(document, "mouseup", done); + }); + + CodeMirror.on(this.node, "click", function(e) { + CodeMirror.e_preventDefault(e); + var innerBox = self.inner.getBoundingClientRect(), where; + if (self.orientation == "horizontal") + where = e.clientX < innerBox.left ? -1 : e.clientX > innerBox.right ? 1 : 0; + else + where = e.clientY < innerBox.top ? -1 : e.clientY > innerBox.bottom ? 1 : 0; + self.moveTo(self.pos + where * self.screen); + }); + + function onWheel(e) { + var moved = CodeMirror.wheelEventPixels(e)[self.orientation == "horizontal" ? "x" : "y"]; + var oldPos = self.pos; + self.moveTo(self.pos + moved); + if (self.pos != oldPos) CodeMirror.e_preventDefault(e); + } + CodeMirror.on(this.node, "mousewheel", onWheel); + CodeMirror.on(this.node, "DOMMouseScroll", onWheel); + } + + Bar.prototype.setPos = function(pos, force) { + if (pos < 0) pos = 0; + if (pos > this.total - this.screen) pos = this.total - this.screen; + if (!force && pos == this.pos) return false; + this.pos = pos; + this.inner.style[this.orientation == "horizontal" ? "left" : "top"] = + (pos * (this.size / this.total)) + "px"; + return true + }; + + Bar.prototype.moveTo = function(pos) { + if (this.setPos(pos)) this.scroll(pos, this.orientation); + } + + var minButtonSize = 10; + + Bar.prototype.update = function(scrollSize, clientSize, barSize) { + var sizeChanged = this.screen != clientSize || this.total != scrollSize || this.size != barSize + if (sizeChanged) { + this.screen = clientSize; + this.total = scrollSize; + this.size = barSize; + } + + var buttonSize = this.screen * (this.size / this.total); + if (buttonSize < minButtonSize) { + this.size -= minButtonSize - buttonSize; + buttonSize = minButtonSize; + } + this.inner.style[this.orientation == "horizontal" ? "width" : "height"] = + buttonSize + "px"; + this.setPos(this.pos, sizeChanged); + }; + + function SimpleScrollbars(cls, place, scroll) { + this.addClass = cls; + this.horiz = new Bar(cls, "horizontal", scroll); + place(this.horiz.node); + this.vert = new Bar(cls, "vertical", scroll); + place(this.vert.node); + this.width = null; + } + + SimpleScrollbars.prototype.update = function(measure) { + if (this.width == null) { + var style = window.getComputedStyle ? window.getComputedStyle(this.horiz.node) : this.horiz.node.currentStyle; + if (style) this.width = parseInt(style.height); + } + var width = this.width || 0; + + var needsH = measure.scrollWidth > measure.clientWidth + 1; + var needsV = measure.scrollHeight > measure.clientHeight + 1; + this.vert.node.style.display = needsV ? "block" : "none"; + this.horiz.node.style.display = needsH ? "block" : "none"; + + if (needsV) { + this.vert.update(measure.scrollHeight, measure.clientHeight, + measure.viewHeight - (needsH ? width : 0)); + this.vert.node.style.bottom = needsH ? width + "px" : "0"; + } + if (needsH) { + this.horiz.update(measure.scrollWidth, measure.clientWidth, + measure.viewWidth - (needsV ? width : 0) - measure.barLeft); + this.horiz.node.style.right = needsV ? width + "px" : "0"; + this.horiz.node.style.left = measure.barLeft + "px"; + } + + return {right: needsV ? width : 0, bottom: needsH ? width : 0}; + }; + + SimpleScrollbars.prototype.setScrollTop = function(pos) { + this.vert.setPos(pos); + }; + + SimpleScrollbars.prototype.setScrollLeft = function(pos) { + this.horiz.setPos(pos); + }; + + SimpleScrollbars.prototype.clear = function() { + var parent = this.horiz.node.parentNode; + parent.removeChild(this.horiz.node); + parent.removeChild(this.vert.node); + }; + + CodeMirror.scrollbarModel.simple = function(place, scroll) { + return new SimpleScrollbars("CodeMirror-simplescroll", place, scroll); + }; + CodeMirror.scrollbarModel.overlay = function(place, scroll) { + return new SimpleScrollbars("CodeMirror-overlayscroll", place, scroll); + }; +}); diff --git a/src/system-polyfills.js b/src/system-polyfills.js new file mode 100644 index 000000000..ca76c2418 --- /dev/null +++ b/src/system-polyfills.js @@ -0,0 +1,5 @@ +/* + * SystemJS Promise Polyfill + */ +!function(t){!function(e){"object"==typeof exports?module.exports=e():"function"==typeof t&&t.amd?t(e):"undefined"!=typeof window?window.Promise=e():"undefined"!=typeof global?global.Promise=e():"undefined"!=typeof self&&(self.Promise=e())}(function(){var t;return function t(e,n,o){function r(u,c){if(!n[u]){if(!e[u]){var f="function"==typeof require&&require;if(!c&&f)return f(u,!0);if(i)return i(u,!0);throw new Error("Cannot find module '"+u+"'")}var s=n[u]={exports:{}};e[u][0].call(s.exports,function(t){var n=e[u][1][t];return r(n?n:t)},s,s.exports,t,e,n,o)}return n[u].exports}for(var i="function"==typeof require&&require,u=0;u=0&&(l.splice(e,1),h("Handled previous rejection ["+t.id+"] "+r.formatObject(t.value)))}function c(t,e){p.push(t,e),null===d&&(d=o(f,0))}function f(){for(d=null;p.length>0;)p.shift()(p.shift())}var s,a=n,h=n;"undefined"!=typeof console&&(s=console,a="undefined"!=typeof s.error?function(t){s.error(t)}:function(t){s.log(t)},h="undefined"!=typeof s.info?function(t){s.info(t)}:function(t){s.log(t)}),t.onPotentiallyUnhandledRejection=function(t){c(i,t)},t.onPotentiallyUnhandledRejectionHandled=function(t){c(u,t)},t.onFatalRejection=function(t){c(e,t.value)};var p=[],l=[],d=null;return t}})}("function"==typeof t&&t.amd?t:function(t){n.exports=t(e)})},{"../env":5,"../format":6}],5:[function(e,n,o){!function(t){"use strict";t(function(t){function e(){return"undefined"!=typeof process&&"[object process]"===Object.prototype.toString.call(process)}function n(){return"function"==typeof MutationObserver&&MutationObserver||"function"==typeof WebKitMutationObserver&&WebKitMutationObserver}function o(t){function e(){var t=n;n=void 0,t()}var n,o=document.createTextNode(""),r=new t(e);r.observe(o,{characterData:!0});var i=0;return function(t){n=t,o.data=i^=1}}var r,i="undefined"!=typeof setTimeout&&setTimeout,u=function(t,e){return setTimeout(t,e)},c=function(t){return clearTimeout(t)},f=function(t){return i(t,0)};if(e())f=function(t){return process.nextTick(t)};else if(r=n())f=o(r);else if(!i){var s=t,a=s("vertx");u=function(t,e){return a.setTimer(e,t)},c=a.cancelTimer,f=a.runOnLoop||a.runOnContext}return{setTimer:u,clearTimer:c,asap:f}})}("function"==typeof t&&t.amd?t:function(t){n.exports=t(e)})},{}],6:[function(e,n,o){!function(t){"use strict";t(function(){function t(t){var n="object"==typeof t&&null!==t&&(t.stack||t.message)?t.stack||t.message:e(t);return t instanceof Error?n:n+" (WARNING: non-Error used)"}function e(t){var e=String(t);return"[object Object]"===e&&"undefined"!=typeof JSON&&(e=n(t,e)),e}function n(t,e){try{return JSON.stringify(t)}catch(t){return e}}return{formatError:t,formatObject:e,tryStringify:n}})}("function"==typeof t&&t.amd?t:function(t){n.exports=t()})},{}],7:[function(e,n,o){!function(t){"use strict";t(function(){return function(t){function e(t,e){this._handler=t===_?e:n(t)}function n(t){function e(t){r.resolve(t)}function n(t){r.reject(t)}function o(t){r.notify(t)}var r=new b;try{t(e,n,o)}catch(t){n(t)}return r}function o(t){return k(t)?t:new e(_,new x(v(t)))}function r(t){return new e(_,new x(new P(t)))}function i(){return $}function u(){return new e(_,new b)}function c(t,e){var n=new b(t.receiver,t.join().context);return new e(_,n)}function f(t){return a(B,null,t)}function s(t,e){return a(M,t,e)}function a(t,n,o){function r(e,r,u){u.resolved||h(o,i,e,t(n,r,e),u)}function i(t,e,n){a[t]=e,0===--s&&n.become(new q(a))}for(var u,c="function"==typeof n?r:i,f=new b,s=o.length>>>0,a=new Array(s),p=0;p0?e(n,i.value,r):(r.become(i),p(t,n+1,i))}else e(n,o,r)}function p(t,e,n){for(var o=e;o0||"function"!=typeof e&&r<0)return new this.constructor(_,o);var i=this._beget(),u=i._handler;return o.chain(u,o.receiver,t,e,n),i},e.prototype.catch=function(t){return this.then(void 0,t)},e.prototype._beget=function(){return c(this._handler,this.constructor)},e.all=f,e.race=d,e._traverse=s,e._visitRemaining=p,_.prototype.when=_.prototype.become=_.prototype.notify=_.prototype.fail=_.prototype._unreport=_.prototype._report=K,_.prototype._state=0,_.prototype.state=function(){return this._state},_.prototype.join=function(){for(var t=this;void 0!==t.handler;)t=t.handler;return t},_.prototype.chain=function(t,e,n,o,r){this.when({resolver:t,receiver:e,fulfilled:n,rejected:o,progress:r})},_.prototype.visit=function(t,e,n,o){this.chain(X,t,e,n,o)},_.prototype.fold=function(t,e,n,o){this.when(new S(t,e,n,o))},A(_,w),w.prototype.become=function(t){t.fail()};var X=new w;A(_,b),b.prototype._state=0,b.prototype.resolve=function(t){this.become(v(t))},b.prototype.reject=function(t){this.resolved||this.become(new P(t))},b.prototype.join=function(){if(!this.resolved)return this;for(var t=this;void 0!==t.handler;)if(t=t.handler,t===this)return this.handler=O();return t},b.prototype.run=function(){var t=this.consumers,e=this.handler;this.handler=this.handler.join(),this.consumers=void 0;for(var n=0;ns.length?(o[s]&&"/"||"")+t.substr(s.length):"")}else{var d=s.split("*");if(d.length>2)throw new TypeError("Only one wildcard in a path is permitted");var f=d[0].length;f>=a&&t.substr(0,d[0].length)==d[0]&&t.substr(t.length-d[1].length)==d[1]&&(a=f,n=s,r=t.substr(d[0].length,t.length-d[1].length-d[0].length))}}var m=o[n];return"string"==typeof r&&(m=m.replace("*",r)),m}function m(e){for(var t=[],r=[],n=0,a=e.length;n",linkSets:[],dependencies:[],metadata:{}}}function a(e,t,r){return new Promise(u({step:r.address?"fetch":"locate",loader:e,moduleName:t,moduleMetadata:r&&r.metadata||{},moduleSource:r.source,moduleAddress:r.address}))}function o(t,r,n,a){return new Promise(function(e,o){e(t.loaderObj.normalize(r,n,a))}).then(function(r){var n;if(t.modules[r])return n=e(r),n.status="linked",n.module=t.modules[r],n;for(var a=0,o=t.loads.length;a0)){var r=e.startingLoad;if(e.loader.loaderObj.execute===!1){for(var n=[].concat(e.loads),a=0,o=n.length;a "'+n.paths[o]+'" uses wildcards which are being deprecated for simpler trailing "/" folder paths.')}if(e.defaultJSExtensions&&(n.defaultJSExtensions=e.defaultJSExtensions,w.call(n,"The defaultJSExtensions configuration option is deprecated, use packages configuration instead.")),e.pluginFirst&&(n.pluginFirst=e.pluginFirst),e.map){var i="";for(var o in e.map){var s=e.map[o];if("string"!=typeof s){i+=(i.length?", ":"")+'"'+o+'"';var l=n.defaultJSExtensions&&".js"!=o.substr(o.length-3,3),u=n.decanonicalize(o);l&&".js"==u.substr(u.length-3,3)&&(u=u.substr(0,u.length-3));var c="";for(var f in n.packages)u.substr(0,f.length)==f&&(!u[f.length]||"/"==u[f.length])&&c.split("/").lengtha&&(r=o,a=n));return r}function t(e,t,r,n,a){if(!n||"/"==n[n.length-1]||a||t.defaultExtension===!1)return n;var o=!1;if(t.meta&&p(t.meta,n,function(e,t,r){if(0==r||e.lastIndexOf("*")!=e.length-1)return o=!0}),!o&&e.meta&&p(e.meta,r+"/"+n,function(e,t,r){if(0==r||e.lastIndexOf("*")!=e.length-1)return o=!0}),o)return n;var i="."+(t.defaultExtension||"js");return n.substr(n.length-i.length)!=i?n+i:n}function r(e,r,n,a,i){if(!a){if(!r.main)return n+(e.defaultJSExtensions?".js":"");a="./"==r.main.substr(0,2)?r.main.substr(2):r.main}if(r.map){var s="./"+a,l=S(r.map,s);if(l||(s="./"+t(e,r,n,a,i),s!="./"+a&&(l=S(r.map,s))),l){var u=o(e,r,n,l,s,i);if(u)return u}}return n+"/"+t(e,r,n,a,i)}function n(e,t,r,n){if("."==e)throw new Error("Package "+r+' has a map entry for "." which is not permitted.');return!(t.substr(0,e.length)==e&&n.length>e.length)}function o(e,r,a,o,i,s){"/"==i[i.length-1]&&(i=i.substr(0,i.length-1));var l=r.map[o];if("object"==typeof l)throw new Error("Synchronous conditional normalization not supported sync normalizing "+o+" in "+a);if(n(o,l,a,i)&&"string"==typeof l){if("."==l)l=a;else if("./"==l.substr(0,2))return a+"/"+t(e,r,a,l.substr(2)+i.substr(o.length),s);return e.normalizeSync(l+i.substr(o.length),a+"/")}}function l(e,r,n,a,o){if(!a){if(!r.main)return Promise.resolve(n+(e.defaultJSExtensions?".js":""));a="./"==r.main.substr(0,2)?r.main.substr(2):r.main}var i,s;return r.map&&(i="./"+a,s=S(r.map,i),s||(i="./"+t(e,r,n,a,o),i!="./"+a&&(s=S(r.map,i)))),(s?d(e,r,n,s,i,o):Promise.resolve()).then(function(i){return i?Promise.resolve(i):Promise.resolve(n+"/"+t(e,r,n,a,o))})}function u(e,r,n,a,o,i,s){if("."==o)o=n;else if("./"==o.substr(0,2))return Promise.resolve(n+"/"+t(e,r,n,o.substr(2)+i.substr(a.length),s)).then(function(t){return C.call(e,t,n+"/")});return e.normalize(o+i.substr(a.length),n+"/")}function d(e,t,r,a,o,i){"/"==o[o.length-1]&&(o=o.substr(0,o.length-1));var s=t.map[a];if("string"==typeof s)return n(a,s,r,o)?u(e,t,r,a,s,o,i):Promise.resolve();if(e.builder)return Promise.resolve(r+"/#:"+o);var l=[],d=[];for(var c in s){var f=z(c);d.push({condition:f,map:s[c]}),l.push(e.import(f.module,r))}return Promise.all(l).then(function(e){for(var t=0;tl&&(l=r),v(s,t,r&&l>r)}),v(r.metadata,s)}o.format&&!r.metadata.loader&&(r.metadata.format=r.metadata.format||o.format)}return t})}})}(),function(){function t(){if(s&&"interactive"===s.script.readyState)return s.load;for(var e=0;e=0;i--){for(var s=a[i],l=0;l100&&!i.metadata.format&&(i.metadata.format="global","traceur"===s.transpiler&&(i.metadata.exports="traceur"),"typescript"===s.transpiler&&(i.metadata.exports="ts")),s._loader.loadedTranspiler=!0),s._loader.loadedTranspilerRuntime===!1&&(i.name!=s.normalizeSync("traceur-runtime")&&i.name!=s.normalizeSync("babel/external-helpers*")||(o.length>100&&(i.metadata.format=i.metadata.format||"global"),s._loader.loadedTranspilerRuntime=!0)),("register"==i.metadata.format||i.metadata.bundle)&&s._loader.loadedTranspilerRuntime!==!0){if("traceur"==s.transpiler&&!e.$traceurRuntime&&i.source.match(n))return s._loader.loadedTranspilerRuntime=s._loader.loadedTranspilerRuntime||!1,s.import("traceur-runtime").then(function(){return o});if("babel"==s.transpiler&&!e.babelHelpers&&i.source.match(a))return s._loader.loadedTranspilerRuntime=s._loader.loadedTranspilerRuntime||!1,s.import("babel/external-helpers").then(function(){return o})}return o})}})}();var ie="undefined"!=typeof self?"self":"global";i("fetch",function(e){return function(t){return t.metadata.exports&&!t.metadata.format&&(t.metadata.format="global"),e.call(this,t)}}),i("instantiate",function(e){return function(t){var r=this;if(t.metadata.format||(t.metadata.format="global"),"global"==t.metadata.format&&!t.metadata.entry){var n=M();t.metadata.entry=n,n.deps=[];for(var a in t.metadata.globals){var o=t.metadata.globals[a];o&&n.deps.push(o)}n.execute=function(e,n,a){var o;if(t.metadata.globals){o={};for(var i in t.metadata.globals)t.metadata.globals[i]&&(o[i]=e(t.metadata.globals[i]))}var s=t.metadata.exports;s&&(t.source+="\n"+ie+'["'+s+'"] = '+s+";");var l=r.get("@@global-helpers").prepareGlobal(a.id,s,o,!!t.metadata.encapsulateGlobal);return ee.call(r,t),l()}}return e.call(this,t)}}),i("reduceRegister_",function(e){return function(t,r){if(r||!t.metadata.exports&&(!A||"global"!=t.metadata.format))return e.call(this,t,r);t.metadata.format="global";var n=t.metadata.entry=M();n.deps=t.metadata.deps;var a=R(t.metadata.exports);n.execute=function(){return a}}}),s(function(t){return function(){function r(t){if(Object.keys)Object.keys(e).forEach(t);else for(var r in e)i.call(e,r)&&t(r)}function n(t){r(function(r){if(U.call(s,r)==-1){try{var n=e[r]}catch(e){s.push(r)}t(r,n)}})}var a=this;t.call(a);var o,i=Object.prototype.hasOwnProperty,s=["_g","sessionStorage","localStorage","clipboardData","frames","frameElement","external","mozAnimationStartTime","webkitStorageInfo","webkitIndexedDB","mozInnerScreenY","mozInnerScreenX"];a.set("@@global-helpers",a.newModule({prepareGlobal:function(t,r,a,i){var s=e.define;e.define=void 0;var l;if(a){l={};for(var u in a)l[u]=e[u],e[u]=a[u]}return r||(o={},n(function(e,t){o[e]=t})),function(){var t,a=r?R(r):{},u=!!r;if(r&&!i||n(function(n,s){o[n]!==s&&"undefined"!=typeof s&&(i&&(e[n]=void 0),r||(a[n]=s,"undefined"!=typeof t?u||t===s||(u=!0):t=s))}),a=u?a:t,l)for(var d in l)e[d]=l[d];return e.define=s,a}}}))}}),function(){function t(e){function t(e,t){for(var r=0;rt.index)return!0;return!1}n.lastIndex=a.lastIndex=o.lastIndex=0;var r,i=[],s=[],l=[];if(e.length/e.split("\n").length<200){for(;r=o.exec(e);)s.push([r.index,r.index+r[0].length]);for(;r=a.exec(e);)t(s,r)||l.push([r.index+r[1].length,r.index+r[0].length-1])}for(;r=n.exec(e);)if(!t(s,r)&&!t(l,r)){var u=r[1].substr(1,r[1].length-2);if(u.match(/"|'/))continue;"/"==u[u.length-1]&&(u=u.substr(0,u.length-1)),i.push(u)}return i}var r=/(?:^\uFEFF?|[^$_a-zA-Z\xA0-\uFFFF.])(exports\s*(\[['"]|\.)|module(\.exports|\['exports'\]|\["exports"\])\s*(\[['"]|[=,\.]))/,n=/(?:^\uFEFF?|[^$_a-zA-Z\xA0-\uFFFF."'])require\s*\(\s*("[^"\\]*(?:\\.[^"\\]*)*"|'[^'\\]*(?:\\.[^'\\]*)*')\s*\)/g,a=/(^|[^\\])(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/gm,o=/("[^"\\\n\r]*(\\.[^"\\\n\r]*)*"|'[^'\\\n\r]*(\\.[^'\\\n\r]*)*')/g,s=/^\#\!.*/;i("instantiate",function(a){return function(o){var i=this;if(o.metadata.format||(r.lastIndex=0,n.lastIndex=0,(n.exec(o.source)||r.exec(o.source))&&(o.metadata.format="cjs")),"cjs"==o.metadata.format){var l=o.metadata.deps,u=o.metadata.cjsRequireDetection===!1?[]:t(o.source);for(var d in o.metadata.globals)o.metadata.globals[d]&&u.push(o.metadata.globals[d]);var c=M();o.metadata.entry=c,c.deps=u,c.executingRequire=!0,c.execute=function(t,r,n){function a(e){return"/"==e[e.length-1]&&(e=e.substr(0,e.length-1)),t.apply(this,arguments)}if(a.resolve=function(e){return i.get("@@cjs-helpers").requireResolve(e,n.id)},n.paths=[],n.require=t,!o.metadata.cjsDeferDepsExecute)for(var u=0;u1;)n=a.shift(),e=e[n]=e[n]||{};n=a.shift(),n in e||(e[n]=r)}s(function(e){return function(){this.meta={},e.call(this)}}),i("locate",function(e){return function(t){var r,n=this.meta,a=t.name,o=0;for(var i in n)if(r=i.indexOf("*"),r!==-1&&i.substr(0,r)===a.substr(0,r)&&i.substr(r+1)===a.substr(a.length-i.length+r+1)){var s=i.split("/").length;s>o&&(o=s),v(t.metadata,n[i],o!=s)}return n[a]&&v(t.metadata,n[a]),e.call(this,t)}});var t=/^(\s*\/\*[^\*]*(\*(?!\/)[^\*]*)*\*\/|\s*\/\/[^\n]*|\s*"[^"]+"\s*;?|\s*'[^']+'\s*;?)+/,r=/\/\*[^\*]*(\*(?!\/)[^\*]*)*\*\/|\/\/[^\n]*|"[^"]+"\s*;?|'[^']+'\s*;?/g;i("translate",function(n){return function(a){if("defined"==a.metadata.format)return a.metadata.deps=a.metadata.deps||[],Promise.resolve(a.source);var o=a.source.match(t);if(o)for(var i=o[0].match(r),s=0;s')}else e()}else if("undefined"!=typeof importScripts){var a="";try{throw new Error("_")}catch(e){e.stack.replace(/(?:at|@).*(http.+):[\d]+:[\d]+/,function(e,t){$__curScript={src:t},a=t.replace(/\/[^\/]*$/,"/")})}t&&importScripts(a+"system-polyfills.js"),e()}else $__curScript="undefined"!=typeof __filename?{src:__filename}:null,e()}(); +//# sourceMappingURL=system.js.map diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 000000000..c0fa2f93c --- /dev/null +++ b/src/util.ts @@ -0,0 +1,165 @@ +import {v4 as rawuuid} from "uuid"; + +//--------------------------------------------------------- +// Misc. Utilities +//--------------------------------------------------------- +export function clone(obj:T):T { + if(typeof obj !== "object") return obj; + if(obj.constructor === Array) { + let neue:T = [] as any; + for(let ix = 0; ix < (obj as any).length; ix++) { + neue[ix] = clone(obj[ix]); + } + return neue; + } else { + let neue:T = {} as any; + for(let key in obj) { + neue[key] = clone(obj[key]); + } + return neue; + } +} + +export function uuid() { + let raw:string = rawuuid(); + let mangled = raw.slice(0, 8) + raw.slice(9, 9 + 4) + raw.slice(-12); + return "⍦" + mangled; +} + +export function sortComparator(a, b) { + if(!a.sort || !b.sort) return 0; + let aSort = a.sort; + let bSort = b.sort; + return aSort === bSort ? 0 : (aSort < bSort ? -1 : 1); +} + +export function debounce(fn:CB, wait:number, leading?:boolean) { + let timeout, context, args; + + let doFn = function doDebounced() { + timeout = undefined; + fn.apply(context, args); + context = undefined; + args = undefined; + } + + let debounced:CB; + if(!leading) { + debounced = function(...argList) { + context = this; + args = argList; + if(timeout) { + window.clearTimeout(timeout); + } + timeout = window.setTimeout(doFn, wait); + } as any; + } else { + debounced = function(...argList) { + context = this; + args = argList; + if(!timeout) { + timeout = window.setTimeout(doFn, wait); + } + } as any; + } + + return debounced; +} + +export function unpad(str:string):string { + if(!str) return str; + let indent = 0; + let neue = ""; + let lines = str.split("\n"); + if(lines[0] == "") lines.shift(); + while(lines[0][indent] == " ") indent++; + let multi = false; + for(let line of lines) { + if(multi) neue += "\n"; + neue += line.substring(indent); + multi = true; + } + return neue; +} + +var _wordChars = {}; +function setupWordChars(_wordChars) { + for(let i = "0".charCodeAt(0); i < "9".charCodeAt(0); i++) + _wordChars[String.fromCharCode(i)] = true; + for(let i = "a".charCodeAt(0); i < "z".charCodeAt(0); i++) + _wordChars[String.fromCharCode(i)] = true; + for(let i = "A".charCodeAt(0); i < "Z".charCodeAt(0); i++) + _wordChars[String.fromCharCode(i)] = true; +} +setupWordChars(_wordChars); + +export function adjustToWordBoundary(ch:number, line:string, direction:"left"|"right"):number { + let neue = ch; + if(direction === "left") { + if(_wordChars[line[ch]]) { + // Expand left to contain any word prefix + while(neue > 0) { + // We check the next character since the start of a range is inclusive. + if(!_wordChars[line[neue - 1]]) break; + neue--; + } + + } else { + // Shrink right to eject any leading whitespace + while(neue < line.length) { + if(_wordChars[line[neue]]) break; + neue++; + } + } + } else { + if(_wordChars[line[ch - 1]]) { + // Expand right to contain any word suffix + while(neue < line.length) { + if(!_wordChars[line[neue]]) break; + neue++; + } + } else { + // Shrink left to eject any trailing whitespace + while(neue > 0) { + if(_wordChars[line[neue - 1]]) break; + neue--; + } + } + } + return neue; +} + +//--------------------------------------------------------- +// CodeMirror utilities +//--------------------------------------------------------- + + +export type Range = CodeMirror.Range; +export type Position = CodeMirror.Position; + +export function isRange(loc:any): loc is Range { + return loc.from !== undefined || loc.to !== undefined; +} + +export function comparePositions(a:Position, b:Position) { + if(a.line === b.line && a.ch === b.ch) return 0; + if(a.line > b.line) return 1; + if(a.line === b.line && a.ch > b.ch) return 1; + return -1; +} + +export function compareRanges(a:Range, b:Range) { + let first = comparePositions(a.from, b.from); + if(first !== 0) return first; + else return comparePositions(a.to, b.to); +} + +export function samePosition(a:Position, b:Position) { + return comparePositions(a, b) === 0; +} + +export function whollyEnclosed(inner:Range, outer:Range) { + let left = comparePositions(inner.from, outer.from); + let right = comparePositions(inner.to, outer.to); + return (left === 1 || left === 0) && (right === -1 || right === 0); +} diff --git a/src/uuid.js b/src/uuid.js new file mode 100644 index 000000000..5e2257f09 --- /dev/null +++ b/src/uuid.js @@ -0,0 +1,245 @@ +// uuid.js +// +// Copyright (c) 2010-2012 Robert Kieffer +// MIT License - http://opensource.org/licenses/mit-license.php + +(function() { + var _global = this; + + // Unique ID creation requires a high quality random # generator. We feature + // detect to determine the best RNG source, normalizing to a function that + // returns 128-bits of randomness, since that's what's usually required + var _rng; + + // Node.js crypto-based RNG - http://nodejs.org/docs/v0.6.2/api/crypto.html + // + // Moderately fast, high quality + if (typeof(_global.require) == 'function') { + try { + var _rb = _global.require('crypto').randomBytes; + _rng = _rb && function() {return _rb(16);}; + } catch(e) {} + } + + if (!_rng && _global.crypto && crypto.getRandomValues) { + // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto + // + // Moderately fast, high quality + var _rnds8 = new Uint8Array(16); + _rng = function whatwgRNG() { + crypto.getRandomValues(_rnds8); + return _rnds8; + }; + } + + if (!_rng) { + // Math.random()-based (RNG) + // + // If all else fails, use Math.random(). It's fast, but is of unspecified + // quality. + var _rnds = new Array(16); + _rng = function() { + for (var i = 0, r; i < 16; i++) { + if ((i & 0x03) === 0) r = Math.random() * 0x100000000; + _rnds[i] = r >>> ((i & 0x03) << 3) & 0xff; + } + + return _rnds; + }; + } + + // Buffer class to use + var BufferClass = typeof(_global.Buffer) == 'function' ? _global.Buffer : Array; + + // Maps for number <-> hex string conversion + var _byteToHex = []; + var _hexToByte = {}; + for (var i = 0; i < 256; i++) { + _byteToHex[i] = (i + 0x100).toString(16).substr(1); + _hexToByte[_byteToHex[i]] = i; + } + + // **`parse()` - Parse a UUID into it's component bytes** + function parse(s, buf, offset) { + var i = (buf && offset) || 0, ii = 0; + + buf = buf || []; + s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) { + if (ii < 16) { // Don't overflow! + buf[i + ii++] = _hexToByte[oct]; + } + }); + + // Zero out remaining bytes if string was short + while (ii < 16) { + buf[i + ii++] = 0; + } + + return buf; + } + + // **`unparse()` - Convert UUID byte array (ala parse()) into a string** + function unparse(buf, offset) { + var i = offset || 0, bth = _byteToHex; + return bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]]; + } + + // **`v1()` - Generate time-based UUID** + // + // Inspired by https://github.com/LiosK/UUID.js + // and http://docs.python.org/library/uuid.html + + // random #'s we need to init node and clockseq + var _seedBytes = _rng(); + + // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1) + var _nodeId = [ + _seedBytes[0] | 0x01, + _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5] + ]; + + // Per 4.2.2, randomize (14 bit) clockseq + var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff; + + // Previous uuid creation time + var _lastMSecs = 0, _lastNSecs = 0; + + // See https://github.com/broofa/node-uuid for API details + function v1(options, buf, offset) { + var i = buf && offset || 0; + var b = buf || []; + + options = options || {}; + + var clockseq = options.clockseq != null ? options.clockseq : _clockseq; + + // UUID timestamps are 100 nano-second units since the Gregorian epoch, + // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so + // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs' + // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00. + var msecs = options.msecs != null ? options.msecs : new Date().getTime(); + + // Per 4.2.1.2, use count of uuid's generated during the current clock + // cycle to simulate higher resolution clock + var nsecs = options.nsecs != null ? options.nsecs : _lastNSecs + 1; + + // Time since last uuid creation (in msecs) + var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000; + + // Per 4.2.1.2, Bump clockseq on clock regression + if (dt < 0 && options.clockseq == null) { + clockseq = clockseq + 1 & 0x3fff; + } + + // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new + // time interval + if ((dt < 0 || msecs > _lastMSecs) && options.nsecs == null) { + nsecs = 0; + } + + // Per 4.2.1.2 Throw error if too many uuids are requested + if (nsecs >= 10000) { + throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec'); + } + + _lastMSecs = msecs; + _lastNSecs = nsecs; + _clockseq = clockseq; + + // Per 4.1.4 - Convert from unix epoch to Gregorian epoch + msecs += 12219292800000; + + // `time_low` + var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000; + b[i++] = tl >>> 24 & 0xff; + b[i++] = tl >>> 16 & 0xff; + b[i++] = tl >>> 8 & 0xff; + b[i++] = tl & 0xff; + + // `time_mid` + var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff; + b[i++] = tmh >>> 8 & 0xff; + b[i++] = tmh & 0xff; + + // `time_high_and_version` + b[i++] = tmh >>> 24 & 0xf | 0x10; // include version + b[i++] = tmh >>> 16 & 0xff; + + // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) + b[i++] = clockseq >>> 8 | 0x80; + + // `clock_seq_low` + b[i++] = clockseq & 0xff; + + // `node` + var node = options.node || _nodeId; + for (var n = 0; n < 6; n++) { + b[i + n] = node[n]; + } + + return buf ? buf : unparse(b); + } + + // **`v4()` - Generate random UUID** + + // See https://github.com/broofa/node-uuid for API details + function v4(options, buf, offset) { + // Deprecated - 'format' argument, as supported in v1.2 + var i = buf && offset || 0; + + if (typeof(options) == 'string') { + buf = options == 'binary' ? new BufferClass(16) : null; + options = null; + } + options = options || {}; + + var rnds = options.random || (options.rng || _rng)(); + + // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` + rnds[6] = (rnds[6] & 0x0f) | 0x40; + rnds[8] = (rnds[8] & 0x3f) | 0x80; + + // Copy bytes to buffer, if provided + if (buf) { + for (var ii = 0; ii < 16; ii++) { + buf[i + ii] = rnds[ii]; + } + } + + return buf || unparse(rnds); + } + + // Export public API + var uuid = v4; + uuid.v1 = v1; + uuid.v4 = v4; + uuid.parse = parse; + uuid.unparse = unparse; + uuid.BufferClass = BufferClass; + + if (typeof define === 'function' && define.amd) { + // Publish as AMD module + define(function() {return uuid;}); + } else if (typeof(module) != 'undefined' && module.exports) { + // Publish as node.js module + module.exports = uuid; + } else { + // Publish as global (in browsers) + var _previousRoot = _global.uuid; + + // **`noConflict()` - (browser only) to reset global 'uuid' var** + uuid.noConflict = function() { + _global.uuid = _previousRoot; + return uuid; + }; + + _global.uuid = uuid; + } +}).call(this); diff --git a/test/all.ts b/test/all.ts new file mode 100644 index 000000000..8ca88e5aa --- /dev/null +++ b/test/all.ts @@ -0,0 +1,3 @@ +import "./join"; +import "./eavs"; +import "./math"; diff --git a/test/eavs.ts b/test/eavs.ts new file mode 100644 index 000000000..eaa1ef6b2 --- /dev/null +++ b/test/eavs.ts @@ -0,0 +1,58 @@ +import * as test from "tape"; + +import {Changes} from "../src/runtime/changes"; +import {TripleIndex, MultiIndex} from "../src/runtime/indexes"; +import {toJS, fromJS} from "../src/runtime/util/eavs"; + +function setup() { + let index = new TripleIndex(0); + let multi = new MultiIndex(); + multi.register("session", index); + let changes = new Changes(multi); + return { index, multi, changes }; +} + +function convert(thing, assert) { + let {index, changes} = setup(); + let id = fromJS(changes, thing, "http", "session"); + changes.commit(); + let reconstituted = toJS(index, id); + assert.deepEqual(reconstituted, thing); +} + +test("converting js objects to eavs and back", (assert) => { + convert({foo: "bar", blah: "baz"}, assert); + assert.end(); +}); + +test("converting js nested objects", (assert) => { + convert({foo: {meh: "meh"}, blah: {beep: "boop"}}, assert); + assert.end(); +}) + +test("converting js arrays", (assert) => { + convert(["a", "b", "c"], assert); + assert.end(); +}) + +test("converting nested js arrays", (assert) => { + convert(["a", ["b", "c", "d"], "e"], assert); + assert.end(); +}) + +test("converting nested js objects and arrays", (assert) => { + convert({ + fips: ["a", ["b", "c", "d"], "e"], + moops: {meeps: "mops"}, + beep: ["boop", 3.45], + }, assert); + assert.end(); +}) + +test("converting with null roundtrips", (assert) => { + convert({ + beep: null, + foo: [1, null, 2] + }, assert); + assert.end(); +}) diff --git a/test/join.ts b/test/join.ts new file mode 100644 index 000000000..55f410c5b --- /dev/null +++ b/test/join.ts @@ -0,0 +1,2760 @@ +import * as test from "tape"; +import {InsertAction, RemoveAction} from "../src/runtime/actions"; +import {evaluate,verify,dedent} from "./shared_functions"; + +test("create a record", (assert) => { + let expected = { + insert: [ ["2", "tag", "person"], ["2", "name", "chris"], ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + ~~~ + `); + assert.end(); +}) + +test("search and create a record", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["5", "tag", "person"], + ["5", "name", "joe"], + ["8|2", "dude", "2"], + ["8|5", "dude", "5"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + [#person name: "joe"] + ~~~ + + foo bar + ~~~ + search + p = [#person] + commit + [dude: p] + ~~~ + `); + assert.end(); +}) + +test("search with constant filter", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["5", "tag", "person"], + ["5", "name", "joe"], + ["9|2", "dude", "2"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + [#person name: "joe"] + ~~~ + + foo bar + ~~~ + search + p = [#person name] + name = "chris" + commit + [dude: p] + ~~~ + `); + assert.end(); +}) + + +test("search with constant attribute", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["5", "tag", "person"], + ["5", "name", "joe"], + ["9|2", "dude", "2"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + [#person name: "joe"] + ~~~ + + foo bar + ~~~ + search + p = [#person name: "chris"] + commit + [dude: p] + ~~~ + `); + assert.end(); +}) + +test("search with attribute having multiple values", (assert) => { + let expected = { + insert: [ + ["3", "tag", "person"], + ["3", "name", "chris"], + ["3", "name", "michael"], + ["6", "tag", "person"], + ["6", "name", "chris"], + ["11|3", "dude", "3"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris" name: "michael"] + [#person name: "chris"] + ~~~ + + foo bar + ~~~ + search + p = [#person name: "chris" name: "michael"] + commit + [dude: p] + ~~~ + `); + assert.end(); +}) + +test("search with attribute having multiple values in parenthesis", (assert) => { + let expected = { + insert: [ + ["3", "tag", "person"], + ["3", "name", "chris"], + ["3", "name", "michael"], + ["8|3", "dude", "3"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris" name: "michael"] + ~~~ + + foo bar + ~~~ + search + p = [#person name: ("chris", "michael")] + commit + [dude: p] + ~~~ + `); + assert.end(); +}) + +test("search with attribute having multiple values in parenthesis with a function", (assert) => { + let expected = { + insert: [ + ["3", "tag", "person"], + ["3", "name", "chris"], + ["3", "name", 13], + ["9|3", "dude", "3"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris" name: 13] + ~~~ + + foo bar + ~~~ + search + p = [#person name: ("chris", 4 + 9)] + commit + [dude: p] + ~~~ + `); + assert.end(); +}) + +test("create a record with numeric attributes", (assert) => { + let expected = { + insert: [ + ["4", "tag", "json-array"], + ["4", 1, "cool"], + ["4", 2, "om nom"], + ["4", 3, "om nom nom"], + ], + remove: [] + }; + evaluate(assert, expected, ` + array + ~~~ + commit + [#json-array 1: "cool" 2: "om nom" 3: "om nom nom"] + ~~~ + `); + assert.end(); +}) + + +test("search a record with numeric attributes", (assert) => { + let expected = { + insert: [ + ["4", "tag", "json-array"], + ["4", 1, "cool"], + ["4", 2, "om nom"], + ["4", 3, "om nom nom"], + ["11","foo","cool - om nom - om nom nom"] + ], + remove: [] + }; + evaluate(assert, expected, ` + array + ~~~ + commit + [#json-array 1: "cool" 2: "om nom" 3: "om nom nom"] + ~~~ + + ~~~ + search + [#json-array 1: first, 2: second, 3: third] + commit + [| foo: "{{first}} - {{second}} - {{third}}"}] + ~~~ + `); + assert.end(); +}) + +test("search with incompatible filters", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["5", "tag", "person"], + ["5", "name", "joe"], + ], + remove: [], + errors: true, + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + [#person name: "joe"] + ~~~ + + foo bar + ~~~ + search + p = [#person name] + name = "chris" + name = "joe" + commit + [dude: p] + ~~~ + `); + assert.end(); +}) + +test("search with unprovided variable", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["5", "tag", "person"], + ["5", "name", "joe"], + ], + remove: [], + errors: true, + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + [#person name: "joe"] + ~~~ + + foo bar + ~~~ + search + [#person] + commit + [dude: p] + ~~~ + `); + assert.end(); +}) + +test("search with unprovided root in an attribute access", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["5", "tag", "person"], + ["5", "name", "joe"], + ], + remove: [], + errors: true, + }; + + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + [#person name: "joe"] + ~~~ + + foo bar + ~~~ + search + [#person] + commit + [dude: p.name] + ~~~ + `); + assert.end(); +}) + +test("search with escaped strings", (assert) => { + let expected = { + insert: [ + ["3", "tag", "person"], + ["3", "name", "chris"], + ["3","info","{\"age\": 10, \"school\": \"Lincoln\"}"], + ["7|{\"age\": 10, \"school\": \"Lincoln\"}","info","{\"age\": 10, \"school\": \"Lincoln\"}"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris" info: "{\\"age\\": 10, \\"school\\": \\"Lincoln\\"}"] + ~~~ + + foo bar + ~~~ + search + [#person info] + commit + [info] + ~~~ + `); + assert.end(); +}) + +test("search with escaped embeds", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["7|{chris}","info","{chris}"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + ~~~ + + foo bar + ~~~ + search + [#person name] + commit + [info: "\\{{{name}}\\}"] + ~~~ + `); + assert.end(); +}) + +test("setting an attribute", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["2", "dude", "chris"], + ["5", "tag", "person"], + ["5", "name", "joe"], + ["5", "dude", "joe"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + [#person name: "joe"] + ~~~ + + foo bar + ~~~ + search + p = [#person name] + commit + p.dude := name + ~~~ + `); + assert.end(); +}); + +test("setting an attribute to itself", (assert) => { + // TODO: should this really be showing name inserted twice? + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["2", "name", "chris"], + ["5", "tag", "person"], + ["5", "name", "joe"], + ["5", "name", "joe"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + [#person name: "joe"] + ~~~ + + foo bar + ~~~ + search + p = [#person name] + commit + p.name := name + ~~~ + `); + assert.end(); +}); + +test("setting an attribute in multiple blocks", (assert) => { + let expected = { + insert: [ + ["1", "tag", "person"], + ["1", "meep", "maup"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person] + ~~~ + + stuff + ~~~ + search + p = [#person not(meep)] + commit + p.meep := "moop" + ~~~ + + foo bar + ~~~ + search + p = [#person meep] + commit + p.meep := "maup" + ~~~ + `); + assert.end(); +}); + + +test("setting an attribute to multiple values", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["2", "dude", "chris"], + ["2", "dude", "foo"], + ["2", "dude", 3], + ["5", "tag", "person"], + ["5", "name", "joe"], + ["5", "dude", "joe"], + ["5", "dude", "foo"], + ["5", "dude", 3], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + [#person name: "joe"] + ~~~ + + foo bar + ~~~ + search + p = [#person name] + commit + p.dude := (name, "foo", 3) + ~~~ + `); + assert.end(); +}); + +test("merging multiple values into an attribute", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["2", "dude", "chris"], + ["2", "dude", "foo"], + ["2", "dude", 3], + ["5", "tag", "person"], + ["5", "name", "joe"], + ["5", "dude", "joe"], + ["5", "dude", "foo"], + ["5", "dude", 3], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + [#person name: "joe"] + ~~~ + + foo bar + ~~~ + search + p = [#person name] + commit + p <- [dude: (name, "foo", 3)] + ~~~ + `); + assert.end(); +}); + +test("merges with subobjects pick up the parent object as part of their projection", (assert) => { + let expected = { + insert: [ + ["a", "tag", "person"], + ["a", "name", "chris"], + ["b", "tag", "person"], + ["b", "name", "chris"], + ["a", "foo", "c"], + ["b", "foo", "d"], + ["c", "tag", "bar"], + ["c", "name", "chris"], + ["d", "tag", "bar"], + ["d", "name", "chris"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + [#person name: "chris"] + ~~~ + + foo bar + ~~~ + search + p = [#person name] + commit + p <- [foo: [#bar name]] + ~~~ + `); + assert.end(); +}); + +test("creating an object with multiple values for an attribute", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["13|chris|8","tag","dude"], + ["13|chris|8","dude","chris"], + ["13|chris|8","dude","foo"], + ["13|chris|8","dude",8], + ["5", "tag", "person"], + ["5", "name", "joe"], + ["13|joe|8","tag","dude"], + ["13|joe|8","dude","joe"], + ["13|joe|8","dude","foo"], + ["13|joe|8","dude",8], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + [#person name: "joe"] + ~~~ + + foo bar + ~~~ + search + p = [#person name] + commit + [#dude dude: (name, "foo", 3 + 5)] + ~~~ + `); + assert.end(); +}); + +test("creating an object with multiple complex values for an attribute", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["6", "tag", "foo"], + ["8", "tag", "bar"], + ["12","tag","dude"], + ["12","dude","6"], + ["12","dude","8"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + ~~~ + + foo bar + ~~~ + search + p = [#person name] + commit + [#dude dude: ([#foo], [#bar])] + ~~~ + `); + assert.end(); +}); + +test("setting an attribute on an object with multiple complex values", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["6", "tag", "foo"], + ["8", "tag", "bar"], + ["2","dude","6"], + ["2","dude","8"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + ~~~ + + foo bar + ~~~ + search + p = [#person name] + commit + p.dude := ([#foo], [#bar]) + ~~~ + `); + assert.end(); +}); + +test("merging an attribute on an object with multiple complex values", (assert) => { + let expected = { + insert: [ + ["a", "tag", "person"], + ["a", "name", "chris"], + ["b", "tag", "foo"], + ["b", "eve-auto-index", 1], + ["c", "tag", "bar"], + ["c", "eve-auto-index", 2], + ["a","dude","b"], + ["a","dude","c"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + ~~~ + + foo bar + ~~~ + search + p = [#person name] + commit + p <- [dude: [#foo] [#bar]] + ~~~ + `); + assert.end(); +}); + +test("setting an attribute that removes a previous value", (assert) => { + let expected = { + insert: [ + ["3", "tag", "person"], + ["3", "name", "chris"], + ["3", "dude", "chris"], + ], + remove: [ + ] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris" dude: "joe"] + ~~~ + + foo bar + ~~~ + search + p = [#person name] + commit + p.dude := name + ~~~ + `); + assert.end(); +}); + + +test("setting an attribute on click", (assert) => { + let expected = { + insert: [ + ["3", "tag", "person"], + ["3", "name", "chris"], + ["3", "dude", "joe"], + ], + remove: [] + }; + let eve = evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris" dude: "joe"] + ~~~ + + foo bar + ~~~ + search + [#click] + p = [#person name] + commit + p.dude := name + ~~~ + `); + let expected2 = { + insert: [ ["3", "dude", "chris"], ["click-event", "tag", "click"] ], + remove: [ ["3", "dude", "joe"], ] + }; + eve.execute(expected2, [new InsertAction("blah", "click-event", "tag", "click")]); + assert.end(); +}); + + +test("erase a record", (assert) => { + let expected = { + insert: [ + ], + remove: [ + ] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris" dude: "joe"] + ~~~ + + foo bar + ~~~ + search + p = [#person name] + commit + p := none + ~~~ + `); + assert.end(); +}); + +test("erase an attribute", (assert) => { + let expected = { + insert: [ + ["4", "tag", "person"] + ], + remove: [ + ] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person age: 19 age: 21 age: 30] + ~~~ + + foo bar + ~~~ + search + p = [#person] + commit + p.age := none + ~~~ + `); + assert.end(); +}); + +test("sum constant", (assert) => { + let expected = { + insert: [ + ["a", "tag", "person"], + ["a", "name", "joe"], + ["b", "tag", "person"], + ["b", "name", "chris"], + ["c", "tag", "total"], + ["c", "total", 2], + ], + remove: [ + ] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "joe"] + [#person name: "chris"] + ~~~ + + foo bar + ~~~ + search + p = [#person] + total = sum[value: 1, given: p] + commit + [#total total] + ~~~ + `); + assert.end(); +}); + +test("sum variable", (assert) => { + let expected = { + insert: [ + ["3", "tag", "person"], + ["3", "name", "joe"], + ["3", "age", 10], + ["7", "tag", "person"], + ["7", "name", "chris"], + ["7", "age", 20], + ["13|30", "tag", "total"], + ["13|30", "total", 30], + ], + remove: [ + ] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "joe" age: 10] + [#person name: "chris" age: 20] + ~~~ + + foo bar + ~~~ + search + p = [#person age] + total = sum[value: age, given: p] + commit + [#total total] + ~~~ + `); + assert.end(); +}); + +test("sum variable with multiple givens", (assert) => { + let expected = { + insert: [ + ["3", "tag", "person"], + ["3", "name", "joe"], + ["3", "age", 10], + ["7", "tag", "person"], + ["7", "name", "chris"], + ["7", "age", 20], + ["13|30", "tag", "total"], + ["13|30", "total", 30], + ], + remove: [ + ] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "joe" age: 10] + [#person name: "chris" age: 20] + ~~~ + + foo bar + ~~~ + search + p = [#person age] + total = sum[value: age, given: (p, age)] + commit + [#total total] + ~~~ + `); + assert.end(); +}); + +test("sum groups", (assert) => { + let expected = { + insert: [ + ["3", "tag", "person"], + ["3", "name", "joe"], + ["3", "age", 10], + ["7", "tag", "person"], + ["7", "name", "chris"], + ["7", "age", 20], + ["11", "tag", "person"], + ["11", "name", "mike"], + ["11", "age", 20], + ["17|1", "tag", "total"], + ["17|1", "total", 1], + ["17|2", "tag", "total"], + ["17|2", "total", 2], + ], + remove: [ + ] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "joe" age: 10] + [#person name: "chris" age: 20] + [#person name: "mike" age: 20] + ~~~ + + foo bar + ~~~ + search + p = [#person age] + total = sum[value: 1, given: p, per: age] + commit + [#total total] + ~~~ + `); + assert.end(); +}); + +test("sum groups with multiple pers", (assert) => { + let expected = { + insert: [ + ["3", "tag", "person"], + ["3", "name", "joe"], + ["3", "age", 10], + ["7", "tag", "person"], + ["7", "name", "chris"], + ["7", "age", 20], + ["11", "tag", "person"], + ["11", "name", "mike"], + ["11", "age", 20], + ["17|1", "tag", "total"], + ["17|1", "total", 1], + // ["18|2", "tag", "total"], + // ["18|2", "total", 2], + ], + remove: [ + ] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "joe" age: 10] + [#person name: "chris" age: 20] + [#person name: "mike" age: 20] + ~~~ + + foo bar + ~~~ + search + p = [#person age] + total = sum[value: 1, given: p, per: (age, p)] + commit + [#total total] + ~~~ + `); + assert.end(); +}); + + + +test("aggregate stratification", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "joe"], + ["5", "tag", "person"], + ["5", "name", "chris"], + ], + remove: [ + ] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "joe"] + [#person name: "chris"] + ~~~ + + foo bar + ~~~ + search + p = [#person] + total = sum[value: 1, given: p] + total > 2 + commit + [#total total] + ~~~ + `); + assert.end(); +}); + + +test("aggregate stratification with results", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "joe"], + ["5", "tag", "person"], + ["5", "name", "chris"], + ["11|12", "tag", "total"], + ["11|12", "total", 12], + ], + remove: [ + ] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "joe"] + [#person name: "chris"] + ~~~ + + foo bar + ~~~ + search + p = [#person] + total = sum[value: 1, given: p] + total-plus-10 = total + 10 + commit + [#total total: total-plus-10] + ~~~ + `); + assert.end(); +}); + +test("aggregate stratification with another aggregate", (assert) => { + let expected = { + insert: [ + ["a", "tag", "person"], + ["a", "name", "joe"], + ["a", "age", 10], + ["7", "tag", "person"], + ["7", "name", "chris"], + ["7", "age", 20], + ["11", "tag", "person"], + ["11", "name", "mike"], + ["11", "age", 20], + ["18|3", "tag", "total"], + ["18|3", "total", 3], + ], + remove: [ + ] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "joe" age: 10] + [#person name: "chris" age: 20] + [#person name: "mike" age: 20] + ~~~ + + foo bar + ~~~ + search + p = [#person age] + total = sum[value: 1, given: p, per: age] + count-all = sum[value: total, given: total] + commit + [#total total: count-all] + ~~~ + `); + assert.end(); +}); + + +test("unstratifiable aggregate", (assert) => { + assert.throws(() => { + let expected = { + insert: [ ], + remove: [ ] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "joe" age: 10] + [#person name: "chris" age: 20] + [#person name: "mike" age: 20] + ~~~ + + foo bar + ~~~ + search + p = [#person age] + total = sum[value: 1, given: count-all, per: age] + count-all = sum[value: total, given: total] + commit + [#total total: count-all] + ~~~ + `); + }, "Unstratifiable aggregates should throw an error"); + assert.end(); +}); + + +test("single argument is", (assert) => { + let expected = { + insert: [ ["7|false|true", "tag", "result"], ["7|false|true", "result", false], ["7|false|true", "result2", true]], + remove: [ ] + }; + evaluate(assert, expected, ` + is test + ~~~ + search + result = is(3 > 4) + result2 = is(3 < 4) + commit + [#result result result2] + ~~~ + `); + assert.end(); +}); + +test("multiple argument is", (assert) => { + let expected = { + insert: [ ["9|true|false", "tag", "result"], ["9|true|false", "result", true], ["9|true|false", "result2", false]], + remove: [ ] + }; + evaluate(assert, expected, ` + is test + ~~~ + search + result = is(5 > 4, 6 != 9) + result2 = is(5 > 4, 6 = 9) + commit + [#result result result2] + ~~~ + `); + assert.end(); +}); + + +test("block order shouldn't matter", (assert) => { + let expected = { + insert: [ + ["7|bye!", "tag", "result"], ["7|bye!", "result", "bye!"], + ["7|hi!", "tag", "result"], ["7|hi!", "result", "hi!"], + ["10", "tag", "foo"], ["10", "value", "hi!"], + ], + remove: [ ] + }; + evaluate(assert, expected, ` + is test + ~~~ + search + result = if [#foo value] then value + else "bye!" + commit + [#result result] + ~~~ + + add a foo + ~~~ + commit + [#foo value: "hi!"] + ~~~ + `); + let expected2 = { + insert: [ + ["10|bye!", "tag", "result"], ["10|bye!", "result", "bye!"], + ["10|hi!", "tag", "result"], ["10|hi!", "result", "hi!"], + ["2", "tag", "foo"], ["2", "value", "hi!"], + ], + remove: [ ] + }; + evaluate(assert, expected2, ` + add a foo + ~~~ + commit + [#foo value: "hi!"] + ~~~ + + is test + ~~~ + search + result = if [#foo value] then value + else "bye!" + commit + [#result result] + ~~~ + `); + assert.end(); +}); + + +test("if with variable", (assert) => { + let expected = { + insert: [ + ["7|bye!", "tag", "result"], ["7|bye!", "result", "bye!"], + ["7|hi!", "tag", "result"], ["7|hi!", "result", "hi!"], + ["10", "tag", "foo"], ["10", "value", "hi!"], + ], + remove: [ ] + }; + evaluate(assert, expected, ` + is test + ~~~ + search + result = if [#foo value] then value + else "bye!" + commit + [#result result] + ~~~ + + add a foo + ~~~ + commit + [#foo value: "hi!"] + ~~~ + + `); + assert.end(); +}); + +test("else with value", (assert) => { + let expected = { + insert: [ ["6|bye!", "tag", "result"], ["6|bye!", "result", "bye!"]], + remove: [ ] + }; + evaluate(assert, expected, ` + is test + ~~~ + search + result = if [#foo] then "hi!" + else "bye!" + commit + [#result result] + ~~~ + `); + assert.end(); +}); + +test("if with constant equality", (assert) => { + let expected = { + insert: [ + ["2", "tag", "foo"], ["2", "value", "hi!"], + ["13|meh", "tag", "result"], ["13|meh", "result", "meh"] + ], + remove: [ ] + }; + evaluate(assert, expected, ` + add a foo + ~~~ + commit + [#foo value: "hi!"] + ~~~ + + is test + ~~~ + search + [#foo value] + result = if value = "yo" then "cool" + else if x = "meh" then x + else "ok" + commit + [#result result] + ~~~ + `); + assert.end(); +}); + +test("if with an aggregate", (assert) => { + let expected = { + insert: [ + ["2", "tag", "foo"], ["2", "value", "hi!"], + ["10|0", "tag", "result"], ["10|0", "result", 0], + ["10|1", "tag", "result"], ["10|1", "result", 1] + ], + remove: [ ] + }; + evaluate(assert, expected, ` + add a foo + ~~~ + commit + [#foo value: "hi!"] + ~~~ + + is test + ~~~ + search + result = if c = count[given: [#foo]] then c + else 0 + commit + [#result result] + ~~~ + `); + assert.end(); +}); + +test("if with an external equality", (assert) => { + let expected = { + insert: [ + ["2", "tag", "foo"], ["2", "value", "hi!"], + ["11|1", "tag", "result"], ["11|1", "result", 1] + ], + remove: [ ] + }; + evaluate(assert, expected, ` + add a foo + ~~~ + commit + [#foo value: "hi!"] + ~~~ + + is test + ~~~ + search + [#foo value] + moof = "hi!" + result = if moof = value then 1 + else 0 + commit + [#result result] + ~~~ + `); + assert.end(); +}); + +test("bind adds results", (assert) => { + let expected = { + insert: [ + ["2", "tag", "foo"], ["2", "value", "hi!"], + ["7|hi!", "tag", "result"], ["7|hi!", "value", "hi!"] + ], + remove: [ ] + }; + evaluate(assert, expected, ` + add a foo + ~~~ + commit + [#foo value: "hi!"] + ~~~ + + is test + ~~~ + search + [#foo value] + bind + [#result value] + ~~~ + `); + assert.end(); +}); + + +test("bind removes dead results", (assert) => { + let expected = { + insert: [ + ["2", "tag", "foo"], + ["2", "value", "hi!"], + ["7|hi!", "tag", "result"], + ["7|hi!", "value", "hi!"] + ], + remove: [ ] + }; + let eve = evaluate(assert, expected, ` + add a foo + ~~~ + commit + [#foo value: "hi!"] + ~~~ + + is test + ~~~ + search + [#foo value] + bind + [#result value] + ~~~ + `); + let expected2 = { + insert: [], + remove: [ + ["2", "tag", "foo"], + ["2", "value", "hi!"], + ["7|hi!", "tag", "result"], + ["7|hi!", "value", "hi!"] + ] + }; + evaluate(assert, expected2, ` + remove foo + ~~~ + search + foo = [#foo] + commit + foo := none + ~~~ + `, eve.session); + assert.end(); +}); + + +test("you only search facts in the specified database", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["5", "tag", "person"], + ["5", "name", "joe"], + // ["9|2", "dude", "2"], + // ["9|5", "dude", "5"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + [#person name: "joe"] + ~~~ + + foo bar + ~~~ + search @foo + p = [#person] + commit + [dude: p] + ~~~ + `); + assert.end(); +}) + + +test("you can search from multiple databases", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["5", "tag", "person"], + ["5", "name", "joe"], + ["8|2", "dude", "2"], + ["8|5", "dude", "5"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + commit @foo + [#person name: "joe"] + ~~~ + + foo bar + ~~~ + search (@foo, @session) + p = [#person] + commit + [dude: p] + ~~~ + `); + assert.end(); +}) + +test("writing is scoped to databases", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["5", "tag", "person"], + ["5", "name", "joe"], + // ["9|2", "dude", "2"], + // ["9|5", "dude", "5"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit @foo + [#person name: "chris"] + [#person name: "joe"] + ~~~ + + foo bar + ~~~ + search + p = [#person] + commit + [dude: p] + ~~~ + `); + assert.end(); +}) + +test("you can write into multiple databases", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["5", "tag", "person"], + ["5", "name", "joe"], + ["2", "tag", "person"], + ["2", "name", "chris"], + ["5", "tag", "person"], + ["5", "name", "joe"], + ["8|2", "dude", "2"], + ["8|5", "dude", "5"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit (@foo, @session) + [#person name: "chris"] + [#person name: "joe"] + ~~~ + + foo bar + ~~~ + search + p = [#person] + commit + [dude: p] + ~~~ + `); + assert.end(); +}) + +test("reading in a scoped write uses the search scope", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["5", "tag", "person"], + ["5", "name", "joe"], + ["9|chris", "dude", "chris"], + ["9|joe", "dude", "joe"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + [#person name: "joe"] + ~~~ + + foo bar + ~~~ + search + p = [#person] + commit @foo + [dude: p.name] + ~~~ + `); + assert.end(); +}) + +test("reading in multiple scopes write uses the search scope", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["5", "tag", "person"], + ["5", "name", "joe"], + ["8", "tag", "person"], + ["8", "name", "woop"], + ["12|chris", "dude", "chris"], + ["12|joe", "dude", "joe"], + ["12|woop", "dude", "woop"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit @blah + [#person name: "chris"] + [#person name: "joe"] + commit + [#person name: "woop"] + ~~~ + + foo bar + ~~~ + search (@blah, @session) + p = [#person] + commit @foo + [dude: p.name] + ~~~ + `); + assert.end(); +}) + +test("scoped attribute mutators pick up the search scope", (assert) => { + let expected = { + insert: [ + ["6", "tag", "person"], + ["6", "name", "chris"], + ["6", "brother", "2|6"], + ["2|6", "tag", "person"], + ["2|6", "name", "ryan"], + ["2|6", "name", "meep"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris" brother: [#person name: "ryan"]] + ~~~ + + foo bar + ~~~ + search + p = [#person] + commit @foo + p.brother.name := "meep" + ~~~ + `); + assert.end(); +}) + +test("multi-level attribute accesses", (assert) => { + let expected = { + insert: [ + ["6", "tag", "person"], + ["6", "name", "chris"], + ["6", "brother", "2|6"], + ["2|6", "tag", "person"], + ["2|6", "name", "ryan"], + ["15|ryan", "tag", "dude"], + ["15|ryan", "dude", "ryan"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris" brother: [#person name: "ryan"]] + ~~~ + + foo bar + ~~~ + search + p = [#person] + p2 = [#person name: p.brother.name] + commit + [#dude dude: p2.name] + ~~~ + `); + assert.end(); +}) + +test("split function", (assert) => { + let expected = { + insert: [ + ["2|foo", "dude", "foo"], + ["2|bar", "dude", "bar"], + ["2|baz", "dude", "baz"], + ], + remove: [] + }; + evaluate(assert, expected, ` + foo bar + ~~~ + search + token = split[text: "foo|bar|baz" by: "|"] + commit + [dude: token] + ~~~ + `); + assert.end(); +}) + + +test("split function with multiple returns", (assert) => { + let expected = { + insert: [ + ["3|foo|1", "dude", "foo"], + ["3|foo|1", "index", 1], + ["3|bar|2", "dude", "bar"], + ["3|bar|2", "index", 2], + ["3|baz|3", "dude", "baz"], + ["3|baz|3", "index", 3], + ], + remove: [] + }; + evaluate(assert, expected, ` + foo bar + ~~~ + search + (token, index) = split[text: "foo|bar|baz" by: "|"] + commit + [dude: token, index] + ~~~ + `); + assert.end(); +}) + + +test("split function with attribute returns", (assert) => { + let expected = { + insert: [ + ["3|foo|1", "dude", "foo"], + ["3|foo|1", "index", 1], + ["3|bar|2", "dude", "bar"], + ["3|bar|2", "index", 2], + ["3|baz|3", "dude", "baz"], + ["3|baz|3", "index", 3], + ], + remove: [] + }; + evaluate(assert, expected, ` + foo bar + ~~~ + search + split[text: "foo|bar|baz" by: "|", token, index] + commit + [dude: token, index] + ~~~ + `); + assert.end(); +}) + +test("split function with fixed return", (assert) => { + let expected = { + insert: [ + ["4|bar", "dude", "bar"], + ["4|bar", "index", 2], + ], + remove: [] + }; + evaluate(assert, expected, ` + foo bar + ~~~ + search + (token, 2) = split[text: "foo|bar|baz" by: "|"] + commit + [dude: token, index: 2] + ~~~ + `); + assert.end(); +}) + +test("split function with fixed return attribute", (assert) => { + let expected = { + insert: [ + ["4|bar", "dude", "bar"], + ["4|bar", "index", 2], + ], + remove: [] + }; + evaluate(assert, expected, ` + foo bar + ~~~ + search + split[text: "foo|bar|baz" by: "|", token, index: 2] + commit + [dude: token, index: 2] + ~~~ + `); + assert.end(); +}) + +test("split function with fixed token", (assert) => { + let expected = { + insert: [ + ["4|2", "dude", "bar"], + ["4|2", "index", 2], + ], + remove: [] + }; + evaluate(assert, expected, ` + foo bar + ~~~ + search + split[text: "foo|bar|baz" by: "|", token: "bar", index] + commit + [dude: "bar", index] + ~~~ + `); + assert.end(); +}) + + +test("split function with both fixed", (assert) => { + let expected = { + insert: [ + ["5", "dude", "bar"], + ["5", "index", 2], + ], + remove: [] + }; + evaluate(assert, expected, ` + foo bar + ~~~ + search + split[text: "foo|bar|baz" by: "|", token: "bar", index: 2] + commit + [dude: "bar", index: 2] + ~~~ + `); + assert.end(); +}) + +test("pipe allows you to select ", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["5", "tag", "person"], + ["5", "name", "joe"], + ["10|2", "dude", "2"], + ["10|2", "name", "chris"], + ["10|5", "dude", "5"], + ["10|5", "name", "joe"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + [#person name: "joe"] + ~~~ + + foo bar + ~~~ + search + p = [#person name] + commit + [dude: p | name] + ~~~ + `); + assert.end(); +}) + +test("lookup with bound record", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["7", "info", "Has tag with value person"], + ["7", "info", "Has name with value chris"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + ~~~ + + foo bar + ~~~ + search + lookup[record: [#person], attribute, value] + commit + [| info: "Has {{attribute}} with value {{value}}"] + ~~~ + `); + assert.end(); +}) + + +test("lookup with bound attribute", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["6", "record", "2"], + ["6", "value", "chris"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + ~~~ + + foo bar + ~~~ + search + lookup[record, attribute: "name", value] + commit + [| record value] + ~~~ + `); + assert.end(); +}) + +test("lookup with free attribute, node and bound value", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["7", "record", "2"], + ["7", "attribute", "name"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + ~~~ + + foo bar + ~~~ + search + record = [#person] + lookup[record, attribute, value: "chris", node] + commit + [| record attribute] + ~~~ + `); + assert.end(); +}) + +test("lookup on node", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["6","record","2"], + ["6","attribute","tag"], + ["6","value","person"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + ~~~ + + foo bar + ~~~ + search + lookup[record, attribute, value, node: "0|block|0|node|3|build"] + commit + [| record attribute value] + ~~~ + `); + assert.end(); +}) + +test("lookup all free", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["6","record","2"], + ["6","attribute","tag"], + ["6","value","person"], + ["6","node","0|block|0|node|3|build"], + ["6","attribute","name"], + ["6","value","chris"], + ["6","node","0|block|0|node|5|build"], + + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + ~~~ + + foo bar + ~~~ + search + lookup[record, attribute, value, node] + commit @foo + [| record attribute value node] + ~~~ + `); + assert.end(); +}); + +test("lookup action", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["2", "woo4", "yep"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + ~~~ + + foo bar + ~~~ + search + record = [#person] + attribute = "woo{{1 + 3}}" + value = "yep" + commit + lookup[record, attribute, value] + ~~~ + `); + assert.end(); +}) + +test("lookup action without value errors", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ], + remove: [], + errors: true, + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + ~~~ + + foo bar + ~~~ + search + record = [#person] + attribute = "woo{{1 + 3}}" + value = "yep" + commit + lookup[record, attribute] + ~~~ + `); + assert.end(); +}) + + +test("lookup action remove", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + ~~~ + + foo bar + ~~~ + search + record = [#person] + attribute = "name" + value = "chris" + commit + lookup[record, attribute, value] := none + ~~~ + `); + assert.end(); +}) + +test("lookup action remove free value", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + ~~~ + + foo bar + ~~~ + search + record = [#person] + attribute = "name" + commit + lookup[record, attribute] := none + ~~~ + `); + assert.end(); +}) + + +test("an identifier followed by whitespace should not be interpreted as a function", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "chris"], + ["2", "dude", "chris"], + ["5", "tag", "person"], + ["5", "name", "joe"], + ["5", "dude", "joe"], + ["10", "tag", "cool"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris"] + [#person name: "joe"] + ~~~ + + foo bar + ~~~ + search + p = [#person name] + commit + p.dude := name + [#cool] + ~~~ + `); + assert.end(); +}); + +test("indented code blocks are not evaled", (assert) => { + let expected = { + insert: [], + remove: [] + }; + evaluate(assert, expected, ` + people + + commit + [#person name: "chris"] + [#person name: "joe"] + + foo bar + ~~~ + search + p = [#person name] + commit + p.dude := name + [#cool] + ~~~ + `); + assert.end(); + }) + +test("single value sort", (assert) => { + let expected = { + insert: [ + ["2", "tag", "person"], + ["2", "name", "a"], + ["5", "tag", "person"], + ["5", "name", "b"], + ["8", "tag", "person"], + ["8", "name", "c"], + ["14|1 a", "dude", "1 a"], + ["14|2 b", "dude", "2 b"], + ["14|3 c", "dude", "3 c"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "a"] + [#person name: "b"] + [#person name: "c"] + ~~~ + + foo bar + ~~~ + search + p = [#person name] + ix = sort[value: name] + commit + [dude: "{{ix}} {{name}}"] + ~~~ + `); + assert.end(); +}) + +test("multi value sort", (assert) => { + let expected = { + insert: [ + ["3", "tag", "person"], + ["3", "name", "a"], + ["3", "age", 1], + ["7", "tag", "person"], + ["7", "name", "a"], + ["7", "age", 2], + ["11", "tag", "person"], + ["11", "name", "b"], + ["11", "age", 1], + ["18|1 a 1", "dude", "1 a 1"], + ["18|2 a 2", "dude", "2 a 2"], + ["18|3 b 1", "dude", "3 b 1"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "a" age: 1] + [#person name: "a" age: 2] + [#person name: "b" age: 1] + ~~~ + + foo bar + ~~~ + search + p = [#person name age] + ix = sort[value: (name, age)] + commit + [dude: "{{ix}} {{name}} {{age}}"] + ~~~ + `); + assert.end(); +}) + +test("multi value sort with multiple directions", (assert) => { + let expected = { + insert: [ + ["3", "tag", "person"], + ["3", "name", "a"], + ["3", "age", 1], + ["7", "tag", "person"], + ["7", "name", "a"], + ["7", "age", 2], + ["11", "tag", "person"], + ["11", "name", "b"], + ["11", "age", 1], + ["18|2 a 1", "dude", "2 a 1"], + ["18|3 a 2", "dude", "3 a 2"], + ["18|1 b 1", "dude", "1 b 1"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "a" age: 1] + [#person name: "a" age: 2] + [#person name: "b" age: 1] + ~~~ + + foo bar + ~~~ + search + p = [#person name age] + ix = sort[value: (name, age), direction: ("down", "up")] + commit + [dude: "{{ix}} {{name}} {{age}}"] + ~~~ + `); + assert.end(); +}) + +test("sort with group", (assert) => { + let expected = { + insert: [ + ["3", "tag", "person"], + ["3", "name", "a"], + ["3", "age", 1], + ["7", "tag", "person"], + ["7", "name", "a"], + ["7", "age", 2], + ["11", "tag", "person"], + ["11", "name", "b"], + ["11", "age", 1], + ["18|1 a 1", "dude", "1 a 1"], + ["18|2 a 2", "dude", "2 a 2"], + ["18|1 b 1", "dude", "1 b 1"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "a" age: 1] + [#person name: "a" age: 2] + [#person name: "b" age: 1] + ~~~ + + foo bar + ~~~ + search + p = [#person name age] + ix = sort[value: age, per: name] + commit + [dude: "{{ix}} {{name}} {{age}}"] + ~~~ + `); + assert.end(); +}) + +test("if with expression-only arguments", (assert) => { + let expected = { + insert: [ + ["7|0", "tag", "div"], + ["7|0", "text", 0], + ], + remove: [] + }; + evaluate(assert, expected, ` + test + ~~~ + search + foo = -1 + 1 + text = if foo < 1 then foo else "baz" + bind @browser + [#div text] + ~~~ + `); + assert.end(); +}) + +test("multiple inequalities in a row", (assert) => { + let expected = { + insert: [ + ["3", "tag", "person"], + ["3", "name", "chris"], + ["3", "age", 20], + ["7", "tag", "person"], + ["7", "name", "joe"], + ["7", "age", 10], + ["14|3", "dude", "3"], + ], + remove: [] + }; + evaluate(assert, expected, ` + people + ~~~ + commit + [#person name: "chris" age: 20] + [#person name: "joe" age: 10] + ~~~ + + foo bar + ~~~ + search + p = [#person name age] + 15 < age < 30 + commit + [dude: p] + ~~~ + `); + assert.end(); +}) + +test("range positive increment", (assert) => { + let expected = { + insert: [ + ["a", "dude", 1], + ["a", "dude", 2], + ["a", "dude", 3], + ["a", "dude", 4], + ["a", "dude", 5], + ], + remove: [] + }; + evaluate(assert, expected, ` + foo bar + ~~~ + search + i = range[from: 1 to: 5] + commit + [| dude: i] + ~~~ + `); + assert.end(); +}) + +test("range negative increment", (assert) => { + let expected = { + insert: [ + ["2", "dude", -1], + ["2", "dude", -2], + ["2", "dude", -3], + ["2", "dude", -4], + ["2", "dude", -5], + ], + remove: [] + }; + evaluate(assert, expected, ` + foo bar + ~~~ + search + i = range[from: -1 to: -5 increment: -1] + commit + [| dude: i] + ~~~ + `); + assert.end(); +}) + +test("range increment on an edge boundary", (assert) => { + let expected = { + insert: [ + ["2", "dude", 1], + ["2", "dude", 4], + ], + remove: [] + }; + evaluate(assert, expected, ` + foo bar + ~~~ + search + i = range[from: 1 to: 5 increment: 3] + commit + [| dude: i] + ~~~ + `); + assert.end(); +}) + +test("range with a single increment", (assert) => { + let expected = { + insert: [ + ["2", "dude", 1], + ], + remove: [] + }; + evaluate(assert, expected, ` + foo bar + ~~~ + search + i = range[from: 1 to: 5 increment: 10] + commit + [| dude: i] + ~~~ + `); + assert.end(); +}) + +test("range with infinite increment", (assert) => { + let expected = { + insert: [], + remove: [] + }; + evaluate(assert, expected, ` + foo bar + ~~~ + search + i = range[from: -1 to: -5 increment: 1] + commit + [| dude: i] + ~~~ + `); + assert.end(); +}) + +test("accessing the same attribute sequence natural joins instead of product joining", (assert) => { + let expected = { + insert: [ + ["a","tag","user"], + ["a","name","Corey Montella"], + ["5","tag","user"], + ["5","name","Chris Granger"], + ["14|2|23","tag","message"], + ["14|2|23","sender","a"], + ["14|2|23","text","Hello, Chris"], + ["14|2|23","eve-auto-index",1], + ["19|5|23","tag","message"], + ["19|5|23","sender","5"], + ["19|5|23","text","Hello there!"], + ["19|5|23","eve-auto-index",2], + ["23","tag","conversation"], + ["23","messages","19|5|23"], + ["23","messages","14|2|23"], + ["34|23","tag","div"], + ["34|23","convos","23"], + ["34|23","text","Chris Granger - Hello there!"], + ["34|23","text","Corey Montella - Hello, Chris"], + ], + remove: [] + }; + evaluate(assert, expected, ` + We have users: + + ~~~ + commit + [#user name: "Corey Montella"] + [#user name: "Chris Granger"] + ~~~ + + And we have conversations with messages between users: + + ~~~ + search + corey = [#user name: "Corey Montella"] + chris = [#user name: "Chris Granger"] + + commit + [#conversation messages: + [#message sender: corey, text: "Hello, Chris"] + [#message sender: chris, text: "Hello there!"]] + ~~~ + + Now I want to display all the messages and their senders + + ~~~ + search + convos = [#conversation] + + bind @browser + [#div convos | text: "{{convos.messages.sender.name}} - {{convos.messages.text}}"] + ~~~ + `); + assert.end(); +}) + +test("not with no external dependencies", (assert) => { + let expected = { + insert: [], + remove: [] + }; + evaluate(assert, expected, ` + foo bar + ~~~ + search + not (9 = 4 + 5) + commit @browser + [#success] + ~~~ + `); + expected = { + insert: [ + ["3", "tag", "success"], + ], + remove: [] + }; + evaluate(assert, expected, ` + foo bar + ~~~ + search + not (2 = 4 + 5) + commit @browser + [#success] + ~~~ + `); + assert.end(); +}) + + +test("not can't provide a variable for an attribute access", (assert) => { + let expected = { + insert: [], + remove: [], + errors: true, + }; + evaluate(assert, expected, ` + foo bar + ~~~ + search + not(threads = [#zom]) + foo = threads.foo + bind + [#foo foo] + ~~~ + `); + assert.end(); +}) + +test("not without dependencies filters correctly", (assert) => { + let expected = { + insert: [[1, "tag", "foo"]], + remove: [], + }; + evaluate(assert, expected, ` + add some stuff + ~~~ + commit + [#foo] + ~~~ + + foo bar + ~~~ + search + not([#foo]) + bind + [#bar] + ~~~ + `); + assert.end(); +}) + + +test("indirect constant equality in if", (assert) => { + let expected = { + insert: [ + ["a", "tag", "div"], + ["a", "text", "1 is true"], + ["b", "tag", "div"], + ["b", "text", "2 is false"], + ["c", "tag", "div"], + ["c", "text", "3 is false"], + ], + remove: [], + }; + evaluate(assert, expected, ` + Now consider this: + + ~~~ + search + one = 1 + x = range[from: 1, to: 3] + value = if x = one then "true" else "false" + + bind @browser + [#div text: "{{x}} is {{value}}"] + ~~~ + `); + assert.end(); +}) + + +test("constant filter in if", (assert) => { + let expected = { + insert: [ + ["a", "tag", "div"], + ["a", "text", 3], + ], + remove: [], + }; + evaluate(assert, expected, ` + Now consider this: + + ~~~ + search + x = 3 + "woohoo" = if x < 3 then "cool" + else if x >= 3 then "woohoo" + + bind @browser + [#div text: x] + ~~~ + `); + assert.end(); +}) + +test("nested if/not expressions correctly get their args set", (assert) => { + let expected = { + insert: [ + ["c", "tag", "item"], + ["c", "idx", 0], + ["c", "title", "title 0"], + ["d", "tag", "item"], + ["d", "idx", 1], + ["a", "tag", "div"], + ["a", "text", "0 - title 0"], + ["b", "tag", "div"], + ["b", "text", "1 - no title"], + ], + remove: [], + }; + evaluate(assert, expected, ` + add a foo + ~~~ + search + item = [#item idx] + title = if not(item.title) then "no title" else item.title + bind @browser + [#div text: "{{idx}} - {{title}}"] + ~~~ + + is test + ~~~ + commit + [#item idx: 0 title: "title 0"] + [#item idx: 1] + ~~~ + `); + assert.end(); +}) diff --git a/test/math.ts b/test/math.ts new file mode 100644 index 000000000..36c9d4a11 --- /dev/null +++ b/test/math.ts @@ -0,0 +1,220 @@ +import * as test from "tape"; +import {evaluate,verify,dedent,testSingleExpressionByList} from "./shared_functions"; + +// Test Constants +// Would this fail on different architectures than mine? Not according to dcmumentation Maybe we Should +// flush out that implicit assumption. + +let constants_list : any = [ + {"Expression":"pi[]", "Value":"3.141592653589793"}, + {"Expression":"e[]", "Value":"2.718281828459045"}, + {"Expression":"ln2[]", "Value":"0.6931471805599453"}, + {"Expression":"log2e[]", "Value":"1.4426950408889634"}, + {"Expression":"log10e[]", "Value":"0.4342944819032518"}, + {"Expression":"sqrt1/2[]", "Value":"0.7071067811865476"}, + {"Expression":"sqrt2[]", "Value":"1.4142135623730951"} +] +testSingleExpressionByList(constants_list); + +// Test Power Function +let pow_list : any = [ + {"Expression":"pow[ value:2 by:3 ]", "Value":"8"}, + {"Expression":"pow[ value:9 by:1 / 2 ]", "Value":"3"}] +testSingleExpressionByList(pow_list); + +// Test Log Function +let log_list : any = [ + {"Expression":"log[ value: e[] ]", "Value":"1"}, + {"Expression":"log[ value: 10 base: 10 ]", "Value":"1"}] +testSingleExpressionByList(log_list); + +// Test Exp Function +let exp_list : any = [ + {"Expression":"exp[ value: 1 ]", "Value":"2.718281828459045"}] +testSingleExpressionByList(exp_list); + +// Test Trig Functions +let trig_list : any = [ + {"Expression":"sin[ radians: 1 ]", "Value":"0.8414709848078965"}, + {"Expression":"cos[ radians: 1 ]", "Value":"0.5403023058681398"}, + {"Expression":"tan[ radians: 1 ]", "Value":"1.5574077246549023"} + ] +testSingleExpressionByList(trig_list); + + +test("Should be able to use the sin function with degrees and radians", (assert) => { + let expected = { + insert: [ + ["a", "tag", "div"], + ["a", "text", "1"], + ["b", "tag", "div"], + ["b", "text", "0.9999996829318346"], + ], + remove: [], + }; + + evaluate(assert, expected, ` + Now consider this: + + ~~~ + search + y = sin[degrees: 90] + x = sin[radians: 3.14 / 2] + + bind @browser + [#div text: y] + [#div text: x] + ~~~ + `); + assert.end(); +}) + +test("Should be able to use the cos function with degrees and radians", (assert) => { + let expected = { + insert: [ + ["a", "tag", "div"], + ["a", "text", "1"], + ["b", "tag", "div"], + ["b", "text", "-0.9999987317275395"], + ], + remove: [], + }; + + evaluate(assert, expected, ` + Now consider this: + + ~~~ + search + y = cos[degrees: 0] + x = cos[radians: 3.14] + + bind @browser + [#div text: y] + [#div text: x] + ~~~ + `); + assert.end(); +}) + +test("Should be able to use the tan function with degrees and radians", (assert) => { + let expected = { + insert: [ + ["a", "tag", "div"], + ["a", "text", "0.5773502691896257"], + ["b", "tag", "div"], + ["b", "text", "0.5463024898437905"], + ], + remove: [], + }; + + evaluate(assert, expected, ` + Now consider this: + + ~~~ + search + y = tan[degrees: 30] + x = tan[radians: 0.5] + + bind @browser + [#div text: y] + [#div text: x] + ~~~ + `); + assert.end(); +}) + +// Test inverse Trig +let atrig_list : any = [ + {"Expression":"asin[ value: 0.8414709848078965 ]", "Value":"1"}, + // Does Eve need an implicit round under the hood? The below should be 1 + {"Expression":"acos[ value: 0.5403023058681398 ]", "Value":"0.9999999999999999"}, + {"Expression":"atan[ value: 1.5574077246549023 ]", "Value":"1"}, + ] +testSingleExpressionByList(atrig_list); + +// Test Hyperbolic Functions +let hyp_list : any = [ + {"Expression":"sinh[ value: 1 ]", "Value":"1.1752011936438014"}, + {"Expression":"cosh[ value: 1 ]", "Value":"1.5430806348152437"}, + {"Expression":"tanh[ value: 1 ]", "Value":"0.7615941559557649"}, + ] +testSingleExpressionByList(hyp_list); + +// Test Inverse Hyperbolic Functions +let ahyp_list : any = [ + {"Expression":"asinh[ value: 1.1752011936438014 ]", "Value":"1"}, + {"Expression":"acosh[ value: 1.5430806348152437 ]", "Value":"1"}, + {"Expression":"atanh[ value: 0.7615941559557649 ]", "Value":"0.9999999999999999"}, + ] +testSingleExpressionByList(ahyp_list); + +test("Range and function within function", (assert) => { + let expected = { + insert: [ + ["a", "tag", "div"], + ["a", "text", "1"], + ["b", "tag", "div"], + ["b", "text", "2"], + ["c", "tag", "div"], + ["c", "text", "3"], + ], + remove: [], + }; + + evaluate(assert, expected, ` + Now consider this: + + ~~~ + search + x = range[from:1 to: pi[] increment: 1 ] + + bind @browser + [#div text:x] + ~~~ + `); + assert.end(); +}) + +// Test Floor Function +let floor_list : any = [ + {"Expression":"floor[ value: 1.0000000000000001 ]", "Value":"1"}, + {"Expression":"floor[ value: 1.999999999999999 ]", "Value":"1"}, + ] +testSingleExpressionByList(floor_list); + +// Test Ceiling Function +let ceiling_list : any = [ + {"Expression":"ceiling[ value: 1.000000000000001 ]", "Value":"2"}, + {"Expression":"ceiling[ value: 1.999999999999999 ]", "Value":"2"}, + ] +testSingleExpressionByList(ceiling_list); + + +// Test ABS Function +let abs_list : any = [ + {"Expression":"abs[ value: -1 ]", "Value":"1"}, + {"Expression":"abs[ value: 1 ]", "Value":"1"}, + ] +testSingleExpressionByList(abs_list); + + +// Test Mod Function +let mod_list : any = [ + {"Expression":"mod[ value: 7 by: 3]", "Value":"1"}, + {"Expression":"mod[ value: 6 by: 3]", "Value":"0"}, + ] +testSingleExpressionByList(mod_list); + + +// Test Round Function +let round_list : any = [ + {"Expression":"round[ value: 1.49999999999999 ]", "Value":"1"}, + {"Expression":"round[ value: 1.5 ]", "Value":"2"} + ] +testSingleExpressionByList(round_list); + +// Test Round Function +let toFixed_list : any = [ + {"Expression":"to-fixed[ value: 1.499 places:2 ]", "Value":"1.50"}, + ] +testSingleExpressionByList(toFixed_list ); diff --git a/test/shared_functions.ts b/test/shared_functions.ts new file mode 100644 index 000000000..044cfdc1a --- /dev/null +++ b/test/shared_functions.ts @@ -0,0 +1,193 @@ +import * as test from "tape"; +import {Evaluation, Database} from "../src/runtime/runtime"; +import * as join from "../src/runtime/join"; +import * as parser from "../src/runtime/parser"; +import * as builder from "../src/runtime/builder"; +import {InsertAction, RemoveAction} from "../src/runtime/actions"; +import {BrowserSessionDatabase} from "../src/runtime/databases/browserSession"; + + +export function dedent(str) { + let lines = []; + let indent; + for(let line of str.split("\n")) { + let match = line.match(/^[ \t]+/); + if(match) { + if(!indent) { + indent = match[0].length; + } + line = line.substr(indent); + } + lines.push(line); + } + return lines.join("\n"); +} + +export function eavsToComparables(eavs, entities, index = {}) { + let results = []; + for(let eav of eavs) { + let [e,a,v] = eav; + let cur = index[e]; + if(!index[e]) { + cur = index[e] = {list: [], links: [], e}; + results.push(cur); + } + if(entities[v]) { + cur.links.push([a, v]); + } else { + let avKey = `${a}, ${v}`; + cur.list.push(avKey); + } + } + return results; +} + +export function isSetEqual(as, bs) { + if(as.length !== bs.length) return false; + for(let a of as) { + if(bs.indexOf(a) === -1) return false; + } + return true; +} + +function collectEntities(eavs, index = {}) { + for(let [e] of eavs) { + index[e] = true; + } + return index; +} + +enum Resolution { + unknown, + resolved, + failed +} + +export function resolveLinks(aLinks, bLinks, entities) { + if(aLinks.length !== bLinks.length) return Resolution.failed; + for(let [a, v] of aLinks) { + let resolved = entities[v]; + if(resolved === true) { + return Resolution.unknown; + } else if(resolved === undefined) { + throw new Error("Found a link for a non entity. " + [a,v]) + } + if(bLinks.some(([a2,v2]) => a2 === a && v2 === resolved).length === 0) { + return Resolution.failed; + } + } + return Resolution.resolved; +} + +export function resolveActualExpected(assert, actuals, expecteds, entities) { + let ix = 0; + let max = actuals.length * actuals.length; + while(actuals[ix]) { + let actual = actuals[ix]; + if(ix === max) { + assert.true(false, "Cyclic test found"); + return; + } + ix++; + let found; + let expectedIx = 0; + for(let expected of expecteds) { + let listEqual, linkEqual; + if(isSetEqual(expected.list, actual.list)) { + listEqual = true; + } else { + found = false; + } + if(actual.links || expected.links) { + let res = resolveLinks(actual.links, expected.links, entities); + if(res === Resolution.failed) { + linkEqual = false; + } else if(res === Resolution.resolved) { + linkEqual = true; + } else { + linkEqual = false; + actuals.push(actual); + break; + } + } else { + linkEqual = true; + } + if(listEqual && linkEqual) { + expecteds.splice(expectedIx, 1); + entities[actual.e] = expected.e; + found = true; + break; + } + expectedIx++; + } + if(found === false) { + assert.true(false, "No matching add found for object: " + JSON.stringify(actual.list)) + } + } +} + +export function verify(assert, adds, removes, data) { + assert.equal(data.insert.length, adds.length, "Wrong number of inserts"); + assert.equal(data.remove.length, removes.length, "Wrong number of removes"); + + // get all the entities + let entities = collectEntities(adds); + entities = collectEntities(data.insert, entities); + entities = collectEntities(removes, entities); + entities = collectEntities(data.remove, entities); + + // + let expectedAdd = eavsToComparables(adds, entities); + let expectedRemove = eavsToComparables(removes, entities); + let actualRemove = eavsToComparables(data.remove, entities); + let actualAdd = eavsToComparables(data.insert, entities); + + resolveActualExpected(assert, actualAdd, expectedAdd, entities); + resolveActualExpected(assert, actualRemove, expectedRemove, entities); +} + +export function evaluate(assert, expected, code, session = new Database()) { + let parsed = parser.parseDoc(dedent(code), "0"); + let {blocks, errors} = builder.buildDoc(parsed.results); + if(expected.errors) { + assert.true(parsed.errors.length > 0 || errors.length > 0, "This test is supposed to produce errors"); + } + session.blocks = session.blocks.concat(blocks); + let evaluation = new Evaluation(); + evaluation.registerDatabase("session", session); + let changes = evaluation.fixpoint(); + verify(assert, expected.insert, expected.remove, changes.result()); + let next = {execute: (expected, actions) => { + let changes = evaluation.executeActions(actions); + verify(assert, expected.insert, expected.remove, changes.result()); + return next; + }, session}; + return next; +} + +export function testSingleExpressionByList(list:any[]){ + list.forEach((list_item,index) =>{ + test(`Is ${list_item.Expression} returning ${list_item.Value}?`, (assert) => { + let expected = { + insert: [ + ["a", "tag", "div"], + ["a", "text", list_item.Value], + ], + remove: [], + }; + + evaluate(assert, expected, ` + Now consider this: + + ~~~ + search + x = ${list_item.Expression} + + bind @browser + [#div text: x] + ~~~ + `); + assert.end(); + }); + }); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..3030a4c9d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "commonjs", + "moduleResolution": "node", + "outDir": "build", + "target": "es5", + "sourceMap": true, + "skipDefaultLibCheck": true, + "forceConsistentCasingInFileNames": true, + "experimentalDecorators" : true + }, + "include": [ "test/**/*.ts", "src/**/*.ts", "src/**/*.js", "scripts/**/*.ts"] +} diff --git a/typings/codemirror/codemirror.d.ts b/typings/codemirror/codemirror.d.ts new file mode 100644 index 000000000..3f3840e67 --- /dev/null +++ b/typings/codemirror/codemirror.d.ts @@ -0,0 +1,1290 @@ +// Type definitions for CodeMirror +// Project: https://github.com/marijnh/CodeMirror +// Definitions by: mihailik +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +declare function CodeMirror(host: HTMLElement, options?: CodeMirror.EditorConfiguration): CodeMirror.Editor; +declare function CodeMirror(callback: (host: HTMLElement) => void , options?: CodeMirror.EditorConfiguration): CodeMirror.Editor; + +declare namespace CodeMirror { + export var Doc : CodeMirror.DocConstructor; + export var Pos: CodeMirror.PositionConstructor; + export var Pass: any; + + function fromTextArea(host: HTMLTextAreaElement, options?: EditorConfiguration): CodeMirror.EditorFromTextArea; + + var version: string; + + /** If you want to define extra methods in terms of the CodeMirror API, it is possible to use defineExtension. + This will cause the given value(usually a method) to be added to all CodeMirror instances created from then on. */ + function defineExtension(name: string, value: any): void; + + /** Like defineExtension, but the method will be added to the interface for Doc objects instead. */ + function defineDocExtension(name: string, value: any): void; + + /** Similarly, defineOption can be used to define new options for CodeMirror. + The updateFunc will be called with the editor instance and the new value when an editor is initialized, + and whenever the option is modified through setOption. */ + function defineOption(name: string, default_: any, updateFunc: Function): void; + + /** If your extention just needs to run some code whenever a CodeMirror instance is initialized, use CodeMirror.defineInitHook. + Give it a function as its only argument, and from then on, that function will be called (with the instance as argument) + whenever a new CodeMirror instance is initialized. */ + function defineInitHook(func: Function): void; + + /** Registers a helper value with the given name in the given namespace (type). This is used to define functionality + that may be looked up by mode. Will create (if it doesn't already exist) a property on the CodeMirror object for + the given type, pointing to an object that maps names to values. I.e. after doing + CodeMirror.registerHelper("hint", "foo", myFoo), the value CodeMirror.hint.foo will point to myFoo. */ + function registerHelper(namespace: string, name: string, helper: any): void; + + + function on(element: any, eventName: string, handler: Function): void; + function off(element: any, eventName: string, handler: Function): void; + + /** Fired whenever a change occurs to the document. changeObj has a similar type as the object passed to the editor's "change" event, + but it never has a next property, because document change events are not batched (whereas editor change events are). */ + function on(doc: Doc, eventName: 'change', handler: (instance: Doc, change: EditorChange) => void ): void; + function off(doc: Doc, eventName: 'change', handler: (instance: Doc, change: EditorChange) => void ): void; + + /** See the description of the same event on editor instances. */ + function on(doc: Doc, eventName: 'beforeChange', handler: (instance: Doc, change: EditorChangeCancellable) => void ): void; + function off(doc: Doc, eventName: 'beforeChange', handler: (instance: Doc, change: EditorChangeCancellable) => void ): void; + + /** Fired whenever the cursor or selection in this document changes. */ + function on(doc: Doc, eventName: 'cursorActivity', handler: (instance: CodeMirror.Editor) => void ): void; + function off(doc: Doc, eventName: 'cursorActivity', handler: (instance: CodeMirror.Editor) => void ): void; + + /** Equivalent to the event by the same name as fired on editor instances. */ + function on(doc: Doc, eventName: 'beforeSelectionChange', handler: (instance: CodeMirror.Editor, selection: { head: Position; anchor: Position; }) => void ): void; + function off(doc: Doc, eventName: 'beforeSelectionChange', handler: (instance: CodeMirror.Editor, selection: { head: Position; anchor: Position; }) => void ): void; + + /** Will be fired when the line object is deleted. A line object is associated with the start of the line. + Mostly useful when you need to find out when your gutter markers on a given line are removed. */ + function on(line: LineHandle, eventName: 'delete', handler: () => void ): void; + function off(line: LineHandle, eventName: 'delete', handler: () => void ): void; + + /** Fires when the line's text content is changed in any way (but the line is not deleted outright). + The change object is similar to the one passed to change event on the editor object. */ + function on(line: LineHandle, eventName: 'change', handler: (line: LineHandle, change: EditorChange) => void ): void; + function off(line: LineHandle, eventName: 'change', handler: (line: LineHandle, change: EditorChange) => void ): void; + + /** Fired when the cursor enters the marked range. From this event handler, the editor state may be inspected but not modified, + with the exception that the range on which the event fires may be cleared. */ + function on(marker: TextMarker, eventName: 'beforeCursorEnter', handler: () => void ): void; + function off(marker: TextMarker, eventName: 'beforeCursorEnter', handler: () => void ): void; + + /** Fired when the range is cleared, either through cursor movement in combination with clearOnEnter or through a call to its clear() method. + Will only be fired once per handle. Note that deleting the range through text editing does not fire this event, + because an undo action might bring the range back into existence. */ + function on(marker: TextMarker, eventName: 'clear', handler: () => void ): void; + function off(marker: TextMarker, eventName: 'clear', handler: () => void ): void; + + /** Fired when the last part of the marker is removed from the document by editing operations. */ + function on(marker: TextMarker, eventName: 'hide', handler: () => void ): void; + function off(marker: TextMarker, eventName: 'hide', handler: () => void ): void; + + /** Fired when, after the marker was removed by editing, a undo operation brought the marker back. */ + function on(marker: TextMarker, eventName: 'unhide', handler: () => void ): void; + function off(marker: TextMarker, eventName: 'unhide', handler: () => void ): void; + + /** Fired whenever the editor re-adds the widget to the DOM. This will happen once right after the widget is added (if it is scrolled into view), + and then again whenever it is scrolled out of view and back in again, or when changes to the editor options + or the line the widget is on require the widget to be redrawn. */ + function on(line: LineWidget, eventName: 'redraw', handler: () => void ): void; + function off(line: LineWidget, eventName: 'redraw', handler: () => void ): void; + + /** Various CodeMirror-related objects emit events, which allow client code to react to various situations. + Handlers for such events can be registered with the on and off methods on the objects that the event fires on. + To fire your own events, use CodeMirror.signal(target, name, args...), where target is a non-DOM-node object. */ + function signal(target: any, name: string, ...args: any[]): void; + + interface Editor { + + /** Tells you whether the editor currently has focus. */ + hasFocus(): boolean; + + /** Used to find the target position for horizontal cursor motion.start is a { line , ch } object, + amount an integer(may be negative), and unit one of the string "char", "column", or "word". + Will return a position that is produced by moving amount times the distance specified by unit. + When visually is true , motion in right - to - left text will be visual rather than logical. + When the motion was clipped by hitting the end or start of the document, the returned value will have a hitSide property set to true. */ + findPosH(start: CodeMirror.Position, amount: number, unit: string, visually: boolean): { line: number; ch: number; hitSide?: boolean; }; + + /** Similar to findPosH , but used for vertical motion.unit may be "line" or "page". + The other arguments and the returned value have the same interpretation as they have in findPosH. */ + findPosV(start: CodeMirror.Position, amount: number, unit: string): { line: number; ch: number; hitSide?: boolean; }; + + + /** Change the configuration of the editor. option should the name of an option, and value should be a valid value for that option. */ + setOption(option: string, value: any): void; + + /** Retrieves the current value of the given option for this editor instance. */ + getOption(option: string): any; + + /** Attach an additional keymap to the editor. + This is mostly useful for add - ons that need to register some key handlers without trampling on the extraKeys option. + Maps added in this way have a higher precedence than the extraKeys and keyMap options, and between them, + the maps added earlier have a lower precedence than those added later, unless the bottom argument was passed, + in which case they end up below other keymaps added with this method. */ + addKeyMap(map: any, bottom?: boolean): void; + + /** Disable a keymap added with addKeyMap.Either pass in the keymap object itself , or a string, + which will be compared against the name property of the active keymaps. */ + removeKeyMap(map: any): void; + + /** Enable a highlighting overlay.This is a stateless mini - mode that can be used to add extra highlighting. + For example, the search add - on uses it to highlight the term that's currently being searched. + mode can be a mode spec or a mode object (an object with a token method). The options parameter is optional. If given, it should be an object. + Currently, only the opaque option is recognized. This defaults to off, but can be given to allow the overlay styling, when not null, + to override the styling of the base mode entirely, instead of the two being applied together. */ + addOverlay(mode: any, options?: any): void; + + /** Pass this the exact argument passed for the mode parameter to addOverlay to remove an overlay again. */ + removeOverlay(mode: any): void; + + + /** Retrieve the currently active document from an editor. */ + getDoc(): CodeMirror.Doc; + + /** Attach a new document to the editor. Returns the old document, which is now no longer associated with an editor. */ + swapDoc(doc: CodeMirror.Doc): CodeMirror.Doc; + + /** Get the content of the current editor document. You can pass it an optional argument to specify the string to be used to separate lines (defaults to "\n"). */ + getValue(seperator?: string): string; + + /** Set the content of the current editor document. */ + setValue(content: string): void; + + /** Sets the gutter marker for the given gutter (identified by its CSS class, see the gutters option) to the given value. + Value can be either null, to clear the marker, or a DOM element, to set it. The DOM element will be shown in the specified gutter next to the specified line. */ + setGutterMarker(line: any, gutterID: string, value: HTMLElement): CodeMirror.LineHandle; + + /** Remove all gutter markers in the gutter with the given ID. */ + clearGutter(gutterID: string): void; + + /** Set a CSS class name for the given line.line can be a number or a line handle. + where determines to which element this class should be applied, can can be one of "text" (the text element, which lies in front of the selection), + "background"(a background element that will be behind the selection), + or "wrap" (the wrapper node that wraps all of the line's elements, including gutter elements). + class should be the name of the class to apply. */ + addLineClass(line: any, where: string, _class_: string): CodeMirror.LineHandle; + + /** Remove a CSS class from a line.line can be a line handle or number. + where should be one of "text", "background", or "wrap"(see addLineClass). + class can be left off to remove all classes for the specified node, or be a string to remove only a specific class. */ + removeLineClass(line: any, where: string, class_: string): CodeMirror.LineHandle; + + /** Returns the line number, text content, and marker status of the given line, which can be either a number or a line handle. */ + lineInfo(line: any): { + line: any; + handle: any; + text: string; + /** Object mapping gutter IDs to marker elements. */ + gutterMarkers: any; + textClass: string; + bgClass: string; + wrapClass: string; + /** Array of line widgets attached to this line. */ + widgets: any; + }; + + /** Puts node, which should be an absolutely positioned DOM node, into the editor, positioned right below the given { line , ch } position. + When scrollIntoView is true, the editor will ensure that the entire node is visible (if possible). + To remove the widget again, simply use DOM methods (move it somewhere else, or call removeChild on its parent). */ + addWidget(pos: CodeMirror.Position, node: HTMLElement, scrollIntoView: boolean): void; + + /** Adds a line widget, an element shown below a line, spanning the whole of the editor's width, and moving the lines below it downwards. + line should be either an integer or a line handle, and node should be a DOM node, which will be displayed below the given line. + options, when given, should be an object that configures the behavior of the widget. + Note that the widget node will become a descendant of nodes with CodeMirror-specific CSS classes, and those classes might in some cases affect it. */ + addLineWidget(line: any, node: HTMLElement, options?: { + /** Whether the widget should cover the gutter. */ + coverGutter?: boolean; + /** Whether the widget should stay fixed in the face of horizontal scrolling. */ + noHScroll?: boolean; + /** Causes the widget to be placed above instead of below the text of the line. */ + above?: boolean; + /** When true, will cause the widget to be rendered even if the line it is associated with is hidden. */ + showIfHidden?: boolean; + }): CodeMirror.LineWidget; + + + /** Programatically set the size of the editor (overriding the applicable CSS rules). + width and height height can be either numbers(interpreted as pixels) or CSS units ("100%", for example). + You can pass null for either of them to indicate that that dimension should not be changed. */ + setSize(width: any, height: any): void; + + /** Scroll the editor to a given(pixel) position.Both arguments may be left as null or undefined to have no effect. */ + scrollTo(x: number, y: number): void; + + /** Get an { left , top , width , height , clientWidth , clientHeight } object that represents the current scroll position, the size of the scrollable area, + and the size of the visible area(minus scrollbars). */ + getScrollInfo(): CodeMirror.ScrollInfo; + + /** Scrolls the given element into view. pos is a { line , ch } position, referring to a given character, null, to refer to the cursor. + The margin parameter is optional. When given, it indicates the amount of pixels around the given area that should be made visible as well. */ + scrollIntoView(pos: CodeMirror.Position, margin?: number): void; + + /** Scrolls the given element into view. pos is a { left , top , right , bottom } object, in editor-local coordinates. + The margin parameter is optional. When given, it indicates the amount of pixels around the given area that should be made visible as well. */ + scrollIntoView(pos: { left: number; top: number; right: number; bottom: number; }, margin: number): void; + + /** Scrolls the given element into view. pos is a { line, ch } object, in editor-local coordinates. + The margin parameter is optional. When given, it indicates the amount of pixels around the given area that should be made visible as well. */ + scrollIntoView(pos: { line: number, ch: number }, margin?: number): void; + + /** Scrolls the given element into view. pos is a { from, to } object, in editor-local coordinates. + The margin parameter is optional. When given, it indicates the amount of pixels around the given area that should be made visible as well. */ + scrollIntoView(pos: { from: CodeMirror.Position, to: CodeMirror.Position }, margin: number): void; + + /** Returns an { left , top , bottom } object containing the coordinates of the cursor position. + If mode is "local" , they will be relative to the top-left corner of the editable document. + If it is "page" or not given, they are relative to the top-left corner of the page. + where is a boolean indicating whether you want the start(true) or the end(false) of the selection. */ + cursorCoords(where: boolean, mode: string): { left: number; top: number; bottom: number; }; + + /** Returns an { left , top , bottom } object containing the coordinates of the cursor position. + If mode is "local" , they will be relative to the top-left corner of the editable document. + If it is "page" or not given, they are relative to the top-left corner of the page. + where specifies the precise position at which you want to measure. */ + cursorCoords(where: CodeMirror.Position, mode: string): { left: number; top: number; bottom: number; }; + + /** Returns the position and dimensions of an arbitrary character.pos should be a { line , ch } object. + This differs from cursorCoords in that it'll give the size of the whole character, + rather than just the position that the cursor would have when it would sit at that position. */ + charCoords(pos: CodeMirror.Position, mode?: string): { left: number; right: number; top: number; bottom: number; }; + + /** Given an { left , top } object , returns the { line , ch } position that corresponds to it. + The optional mode parameter determines relative to what the coordinates are interpreted. It may be "window" , "page"(the default) , or "local". */ + coordsChar(object: { left: number; top: number; }, mode?: string): CodeMirror.Position; + + /** Computes the line at the given pixel height. mode can be one of the same strings that coordsChar accepts. */ + lineAtHeight(height: number, mode?: string): number; + + /** Computes the height of the top of a line, in the coordinate system specified by mode (see coordsChar), which defaults to "page". When a line below the bottom of the document is specified, the returned value is the bottom of the last line in the document. */ + heightAtLine(line: number|LineHandle, mode?: string): number; + + /** Returns the line height of the default font for the editor. */ + defaultTextHeight(): number; + + /** Returns the pixel width of an 'x' in the default font for the editor. + (Note that for non - monospace fonts , this is mostly useless, and even for monospace fonts, non - ascii characters might have a different width). */ + defaultCharWidth(): number; + + /** Returns a { from , to } object indicating the start (inclusive) and end (exclusive) of the currently rendered part of the document. + In big documents, when most content is scrolled out of view, CodeMirror will only render the visible part, and a margin around it. + See also the viewportChange event. */ + getViewport(): { from: number; to: number }; + + /** If your code does something to change the size of the editor element (window resizes are already listened for), or unhides it, + you should probably follow up by calling this method to ensure CodeMirror is still looking as intended. */ + refresh(): void; + + + /** Retrieves information about the token the current mode found before the given position (a {line, ch} object). */ + getTokenAt(pos: CodeMirror.Position): { + /** The character(on the given line) at which the token starts. */ + start: number; + /** The character at which the token ends. */ + end: number; + /** The token's string. */ + string: string; + /** The token type the mode assigned to the token, such as "keyword" or "comment" (may also be null). */ + type: string; + /** The mode's state at the end of this token. */ + state: any; + }; + + /** Returns the mode's parser state, if any, at the end of the given line number. + If no line number is given, the state at the end of the document is returned. + This can be useful for storing parsing errors in the state, or getting other kinds of contextual information for a line. */ + getStateAfter(line?: number): any; + + /** CodeMirror internally buffers changes and only updates its DOM structure after it has finished performing some operation. + If you need to perform a lot of operations on a CodeMirror instance, you can call this method with a function argument. + It will call the function, buffering up all changes, and only doing the expensive update after the function returns. + This can be a lot faster. The return value from this method will be the return value of your function. */ + operation(fn: ()=> T): T; + + /** Adjust the indentation of the given line. + The second argument (which defaults to "smart") may be one of: + "prev" Base indentation on the indentation of the previous line. + "smart" Use the mode's smart indentation if available, behave like "prev" otherwise. + "add" Increase the indentation of the line by one indent unit. + "subtract" Reduce the indentation of the line. */ + indentLine(line: number, dir?: string): void; + + + /** Give the editor focus. */ + focus(): void; + + /** Returns the hidden textarea used to read input. */ + getInputField(): HTMLTextAreaElement; + + /** Returns the DOM node that represents the editor, and controls its size. Remove this from your tree to delete an editor instance. */ + getWrapperElement(): HTMLElement; + + /** Returns the DOM node that is responsible for the scrolling of the editor. */ + getScrollerElement(): HTMLElement; + + /** Fetches the DOM node that contains the editor gutters. */ + getGutterElement(): HTMLElement; + + + + /** Events are registered with the on method (and removed with the off method). + These are the events that fire on the instance object. The name of the event is followed by the arguments that will be passed to the handler. + The instance argument always refers to the editor instance. */ + on(eventName: string, handler: (instance: CodeMirror.Editor) => void ): void; + off(eventName: string, handler: (instance: CodeMirror.Editor) => void ): void; + + /** Fires when a user copies a selection from the editor. */ + on(eventName: 'copy', handler: (instance: CodeMirror.Editor, event: any) => void ): void; + off(eventName: 'copy', handler: (instance: CodeMirror.Editor, event: any) => void ): void; + + /** Fires when a user pastes a selection into the editor. */ + on(eventName: 'paste', handler: (instance: CodeMirror.Editor, event: any) => void ): void; + off(eventName: 'paste', handler: (instance: CodeMirror.Editor, event: any) => void ): void; + + /** Fires every time the content of the editor is changed. */ + on(eventName: 'change', handler: (instance: CodeMirror.Editor, change: CodeMirror.EditorChangeLinkedList) => void ): void; + off(eventName: 'change', handler: (instance: CodeMirror.Editor, change: CodeMirror.EditorChangeLinkedList) => void ): void; + + /** Like the "change" event, but batched per operation, passing an + * array containing all the changes that happened in the operation. + * This event is fired after the operation finished, and display + * changes it makes will trigger a new operation. */ + on(eventName: 'changes', handler: (instance: CodeMirror.Editor, change: CodeMirror.EditorChangeLinkedList[]) => void ): void; + off(eventName: 'changes', handler: (instance: CodeMirror.Editor, change: CodeMirror.EditorChangeLinkedList[]) => void ): void; + + /** This event is fired before a change is applied, and its handler may choose to modify or cancel the change. + The changeObj never has a next property, since this is fired for each individual change, and not batched per operation. + Note: you may not do anything from a "beforeChange" handler that would cause changes to the document or its visualization. + Doing so will, since this handler is called directly from the bowels of the CodeMirror implementation, + probably cause the editor to become corrupted. */ + on(eventName: 'beforeChange', handler: (instance: CodeMirror.Editor, change: CodeMirror.EditorChangeCancellable) => void ): void; + off(eventName: 'beforeChange', handler: (instance: CodeMirror.Editor, change: CodeMirror.EditorChangeCancellable) => void ): void; + + /** Will be fired when the cursor or selection moves, or any change is made to the editor content. */ + on(eventName: 'cursorActivity', handler: (instance: CodeMirror.Editor) => void ): void; + off(eventName: 'cursorActivity', handler: (instance: CodeMirror.Editor) => void ): void; + + /** This event is fired before the selection is moved. Its handler may modify the resulting selection head and anchor. + Handlers for this event have the same restriction as "beforeChange" handlers � they should not do anything to directly update the state of the editor. */ + on(eventName: 'beforeSelectionChange', handler: (instance: CodeMirror.Editor, selection: { head: CodeMirror.Position; anchor: CodeMirror.Position; }) => void ): void; + off(eventName: 'beforeSelectionChange', handler: (instance: CodeMirror.Editor, selection: { head: CodeMirror.Position; anchor: CodeMirror.Position; }) => void ): void; + + /** Fires whenever the view port of the editor changes (due to scrolling, editing, or any other factor). + The from and to arguments give the new start and end of the viewport. */ + on(eventName: 'viewportChange', handler: (instance: CodeMirror.Editor, from: number, to: number) => void ): void; + off(eventName: 'viewportChange', handler: (instance: CodeMirror.Editor, from: number, to: number) => void ): void; + + /** Fires when the editor gutter (the line-number area) is clicked. Will pass the editor instance as first argument, + the (zero-based) number of the line that was clicked as second argument, the CSS class of the gutter that was clicked as third argument, + and the raw mousedown event object as fourth argument. */ + on(eventName: 'gutterClick', handler: (instance: CodeMirror.Editor, line: number, gutter: string, clickEvent: Event) => void ): void; + off(eventName: 'gutterClick', handler: (instance: CodeMirror.Editor, line: number, gutter: string, clickEvent: Event) => void ): void; + + /** Fires whenever the editor is focused. */ + on(eventName: 'focus', handler: (instance: CodeMirror.Editor) => void ): void; + off(eventName: 'focus', handler: (instance: CodeMirror.Editor) => void ): void; + + /** Fires whenever the editor is unfocused. */ + on(eventName: 'blur', handler: (instance: CodeMirror.Editor) => void ): void; + off(eventName: 'blur', handler: (instance: CodeMirror.Editor) => void ): void; + + /** Fires when the editor is scrolled. */ + on(eventName: 'scroll', handler: (instance: CodeMirror.Editor) => void ): void; + off(eventName: 'scroll', handler: (instance: CodeMirror.Editor) => void ): void; + + /** Will be fired whenever CodeMirror updates its DOM display. */ + on(eventName: 'update', handler: (instance: CodeMirror.Editor) => void ): void; + off(eventName: 'update', handler: (instance: CodeMirror.Editor) => void ): void; + + /** Fired whenever a line is (re-)rendered to the DOM. Fired right after the DOM element is built, before it is added to the document. + The handler may mess with the style of the resulting element, or add event handlers, but should not try to change the state of the editor. */ + on(eventName: 'renderLine', handler: (instance: CodeMirror.Editor, line: number, element: HTMLElement) => void ): void; + off(eventName: 'renderLine', handler: (instance: CodeMirror.Editor, line: number, element: HTMLElement) => void ): void; + + /** Expose the state object, so that the Editor.state.completionActive property is reachable*/ + state: any; + } + + interface EditorFromTextArea extends Editor { + + /** Copy the content of the editor into the textarea. */ + save(): void; + + /** Remove the editor, and restore the original textarea (with the editor's current content). */ + toTextArea(): void; + + /** Returns the textarea that the instance was based on. */ + getTextArea(): HTMLTextAreaElement; + } + + interface DocConstructor { + new (text: string, mode?: any, firstLineNumber?: number, lineSep?: string): Doc; + (text: string, mode?: any, firstLineNumber?: number, lineSep?: string): Doc; + } + + interface Doc { + /** Get the current editor content. You can pass it an optional argument to specify the string to be used to separate lines (defaults to "\n"). */ + getValue(seperator?: string): string; + + /** Set the editor content. */ + setValue(content: string): void; + + /** Get the text between the given points in the editor, which should be {line, ch} objects. + An optional third argument can be given to indicate the line separator string to use (defaults to "\n"). */ + getRange(from: Position, to: CodeMirror.Position, seperator?: string): string; + + /** Replace the part of the document between from and to with the given string. + from and to must be {line, ch} objects. to can be left off to simply insert the string at position from. */ + replaceRange(replacement: string, from: CodeMirror.Position, to: CodeMirror.Position, origin?:string): void; + + /** Get the content of line n. */ + getLine(n: number): string; + + /** Set the content of line n. */ + setLine(n: number, text: string): void; + + /** Remove the given line from the document. */ + removeLine(n: number): void; + + /** Get the number of lines in the editor. */ + lineCount(): number; + + /** Get the first line of the editor. This will usually be zero but for linked sub-views, + or documents instantiated with a non-zero first line, it might return other values. */ + firstLine(): number; + + /** Get the last line of the editor. This will usually be lineCount() - 1, but for linked sub-views, it might return other values. */ + lastLine(): number; + + /** Fetches the line handle for the given line number. */ + getLineHandle(num: number): CodeMirror.LineHandle; + + /** Given a line handle, returns the current position of that line (or null when it is no longer in the document). */ + getLineNumber(handle: CodeMirror.LineHandle): number; + + /** Iterate over the whole document, and call f for each line, passing the line handle. + This is a faster way to visit a range of line handlers than calling getLineHandle for each of them. + Note that line handles have a text property containing the line's content (as a string). */ + eachLine(f: (line: CodeMirror.LineHandle) => void ): void; + + /** Iterate over the range from start up to (not including) end, and call f for each line, passing the line handle. + This is a faster way to visit a range of line handlers than calling getLineHandle for each of them. + Note that line handles have a text property containing the line's content (as a string). */ + eachLine(start: number, end: number, f: (line: CodeMirror.LineHandle) => void ): void; + + /** Set the editor content as 'clean', a flag that it will retain until it is edited, and which will be set again when such an edit is undone again. + Useful to track whether the content needs to be saved. */ + markClean(): void; + + /** Returns whether the document is currently clean (not modified since initialization or the last call to markClean). */ + isClean(): boolean; + + + + /** Get the currently selected code. */ + getSelection(): string; + + /** Replace the selection with the given string. By default, the new selection will span the inserted text. + The optional collapse argument can be used to change this � passing "start" or "end" will collapse the selection to the start or end of the inserted text. */ + replaceSelection(replacement: string, collapse?: string): void; + + /** start is a an optional string indicating which end of the selection to return. + It may be "start" , "end" , "head"(the side of the selection that moves when you press shift + arrow), + or "anchor"(the fixed side of the selection).Omitting the argument is the same as passing "head".A { line , ch } object will be returned. */ + getCursor(start?: string): CodeMirror.Position; + + /** Retrieves a list of all current selections. These will always be sorted, and never overlap (overlapping selections are merged). + Each object in the array contains anchor and head properties referring to {line, ch} objects. */ + listSelections(): { anchor: CodeMirror.Position; head: CodeMirror.Position }[]; + + /** Return true if any text is selected. */ + somethingSelected(): boolean; + + /** Set the cursor position.You can either pass a single { line , ch } object , or the line and the character as two separate parameters. */ + setCursor(pos: CodeMirror.Position): void; + + /** Set the selection range.anchor and head should be { line , ch } objects.head defaults to anchor when not given. */ + setSelection(anchor: CodeMirror.Position, head: CodeMirror.Position): void; + + /** Similar to setSelection , but will, if shift is held or the extending flag is set, + move the head of the selection while leaving the anchor at its current place. + pos2 is optional , and can be passed to ensure a region (for example a word or paragraph) will end up selected + (in addition to whatever lies between that region and the current anchor). */ + extendSelection(from: CodeMirror.Position, to?: CodeMirror.Position): void; + + /** Sets or clears the 'extending' flag , which acts similar to the shift key, + in that it will cause cursor movement and calls to extendSelection to leave the selection anchor in place. */ + setExtending(value: boolean): void; + + + /** Retrieve the editor associated with a document. May return null. */ + getEditor(): CodeMirror.Editor; + + + /** Create an identical copy of the given doc. When copyHistory is true , the history will also be copied.Can not be called directly on an editor. */ + copy(copyHistory: boolean): CodeMirror.Doc; + + /** Create a new document that's linked to the target document. Linked documents will stay in sync (changes to one are also applied to the other) until unlinked. */ + linkedDoc(options: { + /** When turned on, the linked copy will share an undo history with the original. + Thus, something done in one of the two can be undone in the other, and vice versa. */ + sharedHist?: boolean; + from?: number; + /** Can be given to make the new document a subview of the original. Subviews only show a given range of lines. + Note that line coordinates inside the subview will be consistent with those of the parent, + so that for example a subview starting at line 10 will refer to its first line as line 10, not 0. */ + to?: number; + /** By default, the new document inherits the mode of the parent. This option can be set to a mode spec to give it a different mode. */ + mode: any; + }): CodeMirror.Doc; + + /** Break the link between two documents. After calling this , changes will no longer propagate between the documents, + and, if they had a shared history, the history will become separate. */ + unlinkDoc(doc: CodeMirror.Doc): void; + + /** Will call the given function for all documents linked to the target document. It will be passed two arguments, + the linked document and a boolean indicating whether that document shares history with the target. */ + iterLinkedDocs(fn: (doc: CodeMirror.Doc, sharedHist: boolean) => void ): void; + + /** Undo one edit (if any undo events are stored). */ + undo(): void; + + /** Redo one undone edit. */ + redo(): void; + + /** Returns an object with {undo, redo } properties , both of which hold integers , indicating the amount of stored undo and redo operations. */ + historySize(): { undo: number; redo: number; }; + + /** Clears the editor's undo history. */ + clearHistory(): void; + + /** Get a(JSON - serializeable) representation of the undo history. */ + getHistory(): any; + + /** Replace the editor's undo history with the one provided, which must be a value as returned by getHistory. + Note that this will have entirely undefined results if the editor content isn't also the same as it was when getHistory was called. */ + setHistory(history: any): void; + + + /** Can be used to mark a range of text with a specific CSS class name. from and to should be { line , ch } objects. */ + markText(from: CodeMirror.Position, to: CodeMirror.Position, options?: CodeMirror.TextMarkerOptions): TextMarker; + + /** Inserts a bookmark, a handle that follows the text around it as it is being edited, at the given position. + A bookmark has two methods find() and clear(). The first returns the current position of the bookmark, if it is still in the document, + and the second explicitly removes the bookmark. */ + setBookmark(pos: CodeMirror.Position, options?: { + /** Can be used to display a DOM node at the current location of the bookmark (analogous to the replacedWith option to markText). */ + widget?: HTMLElement; + + /** By default, text typed when the cursor is on top of the bookmark will end up to the right of the bookmark. + Set this option to true to make it go to the left instead. */ + insertLeft?: boolean; + }): CodeMirror.TextMarker; + + /** Returns an array of all the bookmarks and marked ranges found between the given positions. */ + findMarks(from: CodeMirror.Position, to: CodeMirror.Position): TextMarker[]; + + /** Returns an array of all the bookmarks and marked ranges present at the given position. */ + findMarksAt(pos: CodeMirror.Position): TextMarker[]; + + /** Returns an array containing all marked ranges in the document. */ + getAllMarks(): CodeMirror.TextMarker[]; + + + /** Gets the mode object for the editor. Note that this is distinct from getOption("mode"), which gives you the mode specification, + rather than the resolved, instantiated mode object. */ + getMode(): any; + + /** Calculates and returns a { line , ch } object for a zero-based index whose value is relative to the start of the editor's text. + If the index is out of range of the text then the returned object is clipped to start or end of the text respectively. */ + posFromIndex(index: number): CodeMirror.Position; + + /** The reverse of posFromIndex. */ + indexFromPos(object: CodeMirror.Position): number; + + /** Expose the state object, so that the Doc.state.completionActive property is reachable*/ + state: any; + } + + interface LineHandle { + text: string; + } + + interface ScrollInfo { + left: any; + top: any; + width: any; + height: any; + clientWidth: any; + clientHeight: any; + } + + interface TextMarker { + /** Remove the mark. */ + clear(): void; + + /** Returns a {from, to} object (both holding document positions), indicating the current position of the marked range, + or undefined if the marker is no longer in the document. */ + find(): CodeMirror.Range|CodeMirror.Position; + /** Returns an object representing the options for the marker. If copyWidget is given true, it will clone the value of the replacedWith option, if any. */ + getOptions(copyWidget: boolean): CodeMirror.TextMarkerOptions; + } + + interface LineWidget { + /** Removes the widget. */ + clear(): void; + + /** Call this if you made some change to the widget's DOM node that might affect its height. + It'll force CodeMirror to update the height of the line that contains the widget. */ + changed(): void; + } + + interface EditorChange { + /** Position (in the pre-change coordinate system) where the change started. */ + from: CodeMirror.Position; + /** Position (in the pre-change coordinate system) where the change ended. */ + to: CodeMirror.Position; + /** Array of strings representing the text that replaced the changed range (split by line). */ + text: string[]; + /** Text that used to be between from and to, which is overwritten by this change. */ + removed: string[]; + /** String representing the origin of the change event and wether it can be merged with history */ + origin: string; + } + + interface EditorChangeLinkedList extends CodeMirror.EditorChange { + /** Points to another change object (which may point to another, etc). */ + next?: CodeMirror.EditorChangeLinkedList; + } + + interface EditorChangeCancellable extends CodeMirror.EditorChange { + /** may be used to modify the change. All three arguments to update are optional, and can be left off to leave the existing value for that field intact. */ + update(from?: CodeMirror.Position, to?: CodeMirror.Position, text?: string): void; + + cancel(): void; + + canceled: boolean; + } + + interface PositionConstructor { + new (line: number, ch?: number): Position; + (line: number, ch?: number): Position; + } + + interface Range{ + from: CodeMirror.Position; + to: CodeMirror.Position; + } + + interface Position { + ch: number; + line: number; + } + + interface EditorConfiguration { + /** string| The starting value of the editor. Can be a string, or a document object. */ + value?: any; + + /** string|object. The mode to use. When not given, this will default to the first mode that was loaded. + It may be a string, which either simply names the mode or is a MIME type associated with the mode. + Alternatively, it may be an object containing configuration options for the mode, + with a name property that names the mode (for example {name: "javascript", json: true}). */ + mode?: any; + + /** The theme to style the editor with. You must make sure the CSS file defining the corresponding .cm-s-[name] styles is loaded. + The default is "default". */ + theme?: string; + + /** How many spaces a block (whatever that means in the edited language) should be indented. The default is 2. */ + indentUnit?: number; + + /** Whether to use the context-sensitive indentation that the mode provides (or just indent the same as the line before). Defaults to true. */ + smartIndent?: boolean; + + /** The width of a tab character. Defaults to 4. */ + tabSize?: number; + + /** Whether, when indenting, the first N*tabSize spaces should be replaced by N tabs. Default is false. */ + indentWithTabs?: boolean; + + /** Configures whether the editor should re-indent the current line when a character is typed + that might change its proper indentation (only works if the mode supports indentation). Default is true. */ + electricChars?: boolean; + + /** Determines whether horizontal cursor movement through right-to-left (Arabic, Hebrew) text + is visual (pressing the left arrow moves the cursor left) + or logical (pressing the left arrow moves to the next lower index in the string, which is visually right in right-to-left text). + The default is false on Windows, and true on other platforms. */ + rtlMoveVisually?: boolean; + + /** Configures the keymap to use. The default is "default", which is the only keymap defined in codemirror.js itself. + Extra keymaps are found in the keymap directory. See the section on keymaps for more information. */ + keyMap?: string; + + /** Can be used to specify extra keybindings for the editor, alongside the ones defined by keyMap. Should be either null, or a valid keymap value. */ + extraKeys?: any; + + /** Whether CodeMirror should scroll or wrap for long lines. Defaults to false (scroll). */ + lineWrapping?: boolean; + + /** Whether to show line numbers to the left of the editor. */ + lineNumbers?: boolean; + + /** At which number to start counting lines. Default is 1. */ + firstLineNumber?: number; + + /** A function used to format line numbers. The function is passed the line number, and should return a string that will be shown in the gutter. */ + lineNumberFormatter?: (line: number) => string; + + /** Can be used to add extra gutters (beyond or instead of the line number gutter). + Should be an array of CSS class names, each of which defines a width (and optionally a background), + and which will be used to draw the background of the gutters. + May include the CodeMirror-linenumbers class, in order to explicitly set the position of the line number gutter + (it will default to be to the right of all other gutters). These class names are the keys passed to setGutterMarker. */ + gutters?: string[]; + + /** Determines whether the gutter scrolls along with the content horizontally (false) + or whether it stays fixed during horizontal scrolling (true, the default). */ + fixedGutter?: boolean; + + /** boolean|string. This disables editing of the editor content by the user. If the special value "nocursor" is given (instead of simply true), focusing of the editor is also disallowed. */ + readOnly?: any; + + /**Whether the cursor should be drawn when a selection is active. Defaults to false. */ + showCursorWhenSelecting?: boolean; + + /** The maximum number of undo levels that the editor stores. Defaults to 40. */ + undoDepth?: number; + + /** The period of inactivity (in milliseconds) that will cause a new history event to be started when typing or deleting. Defaults to 500. */ + historyEventDelay?: number; + + /** The tab index to assign to the editor. If not given, no tab index will be assigned. */ + tabindex?: number; + + /** Can be used to make CodeMirror focus itself on initialization. Defaults to off. + When fromTextArea is used, and no explicit value is given for this option, it will be set to true when either the source textarea is focused, + or it has an autofocus attribute and no other element is focused. */ + autofocus?: boolean; + + /** Controls whether drag-and - drop is enabled. On by default. */ + dragDrop?: boolean; + + /** When given , this will be called when the editor is handling a dragenter , dragover , or drop event. + It will be passed the editor instance and the event object as arguments. + The callback can choose to handle the event itself , in which case it should return true to indicate that CodeMirror should not do anything further. */ + onDragEvent?: (instance: CodeMirror.Editor, event: Event) => boolean; + + /** This provides a rather low - level hook into CodeMirror's key handling. + If provided, this function will be called on every keydown, keyup, and keypress event that CodeMirror captures. + It will be passed two arguments, the editor instance and the key event. + This key event is pretty much the raw key event, except that a stop() method is always added to it. + You could feed it to, for example, jQuery.Event to further normalize it. + This function can inspect the key event, and handle it if it wants to. + It may return true to tell CodeMirror to ignore the event. + Be wary that, on some browsers, stopping a keydown does not stop the keypress from firing, whereas on others it does. + If you respond to an event, you should probably inspect its type property and only do something when it is keydown + (or keypress for actions that need character data). */ + onKeyEvent?: (instance: CodeMirror.Editor, event: Event) => boolean; + + /** Half - period in milliseconds used for cursor blinking. The default blink rate is 530ms. */ + cursorBlinkRate?: number; + + /** Determines the height of the cursor. Default is 1 , meaning it spans the whole height of the line. + For some fonts (and by some tastes) a smaller height (for example 0.85), + which causes the cursor to not reach all the way to the bottom of the line, looks better */ + cursorHeight?: number; + + /** Highlighting is done by a pseudo background - thread that will work for workTime milliseconds, + and then use timeout to sleep for workDelay milliseconds. + The defaults are 200 and 300, you can change these options to make the highlighting more or less aggressive. */ + workTime?: number; + + /** See workTime. */ + workDelay?: number; + + /** Indicates how quickly CodeMirror should poll its input textarea for changes(when focused). + Most input is captured by events, but some things, like IME input on some browsers, don't generate events that allow CodeMirror to properly detect it. + Thus, it polls. Default is 100 milliseconds. */ + pollInterval?: number + + /** By default, CodeMirror will combine adjacent tokens into a single span if they have the same class. + This will result in a simpler DOM tree, and thus perform better. With some kinds of styling(such as rounded corners), + this will change the way the document looks. You can set this option to false to disable this behavior. */ + flattenSpans?: boolean; + + /** When highlighting long lines, in order to stay responsive, the editor will give up and simply style + the rest of the line as plain text when it reaches a certain position. The default is 10000. + You can set this to Infinity to turn off this behavior. */ + maxHighlightLength?: number; + + /** Specifies the amount of lines that are rendered above and below the part of the document that's currently scrolled into view. + This affects the amount of updates needed when scrolling, and the amount of work that such an update does. + You should usually leave it at its default, 10. Can be set to Infinity to make sure the whole document is always rendered, + and thus the browser's text search works on it. This will have bad effects on performance of big documents. */ + viewportMargin?: number; + + /** Optional lint configuration to be used in conjunction with CodeMirror's linter addon. */ + lint?: boolean | LintOptions; + + /** Optional value to be used in conjunction with CodeMirror’s placeholder add-on. */ + placeholder?: string; + + + /** Which scrollbar style to use. CodeMirror provides "native" and "null", with "native" being the default. Addons may add more styles. */ + scrollbarStyle?: string; + + // whether you can scroll past the end of the document + scrollPastEnd?: boolean + } + + interface TextMarkerOptions { + /** Assigns a CSS class to the marked stretch of text. */ + className?: string; + + /** Determines whether text inserted on the left of the marker will end up inside or outside of it. */ + inclusiveLeft?: boolean; + + /** Like inclusiveLeft , but for the right side. */ + inclusiveRight?: boolean; + + /** Atomic ranges act as a single unit when cursor movement is concerned — i.e. it is impossible to place the cursor inside of them. + In atomic ranges, inclusiveLeft and inclusiveRight have a different meaning — they will prevent the cursor from being placed + respectively directly before and directly after the range. */ + atomic?: boolean; + + /** Collapsed ranges do not show up in the display.Setting a range to be collapsed will automatically make it atomic. */ + collapsed?: boolean; + + /** When enabled, will cause the mark to clear itself whenever the cursor enters its range. + This is mostly useful for text - replacement widgets that need to 'snap open' when the user tries to edit them. + The "clear" event fired on the range handle can be used to be notified when this happens. */ + clearOnEnter?: boolean; + + /** Determines whether the mark is automatically cleared when it becomes empty. Default is true. */ + clearWhenEmpty?: boolean; + + /** Use a given node to display this range.Implies both collapsed and atomic. + The given DOM node must be an inline element(as opposed to a block element). */ + replacedWith?: HTMLElement; + + /** When replacedWith is given, this determines whether the editor will + * capture mouse and drag events occurring in this widget. Default is + * false—the events will be left alone for the default browser handler, + * or specific handlers on the widget, to capture. */ + handleMouseEvents?: boolean; + + /** A read - only span can, as long as it is not cleared, not be modified except by calling setValue to reset the whole document. + Note: adding a read - only span currently clears the undo history of the editor, + because existing undo events being partially nullified by read - only spans would corrupt the history (in the current implementation). */ + readOnly?: boolean; + + /** When set to true (default is false), adding this marker will create an event in the undo history that can be individually undone(clearing the marker). */ + addToHistory?: boolean; + + /** Can be used to specify an extra CSS class to be applied to the leftmost span that is part of the marker. */ + startStyle?: string; + + /** Equivalent to startStyle, but for the rightmost span. */ + endStyle?: string; + + /** A string of CSS to be applied to the covered text. For example "color: #fe3". */ + css?: string; + + /** When given, will give the nodes created for this span a HTML title attribute with the given value. */ + title?: string; + + /** When the target document is linked to other documents, you can set shared to true to make the marker appear in all documents. + By default, a marker appears only in its target document. */ + shared?: boolean; + } + + interface StringStream { + lastColumnPos: number; + lastColumnValue: number; + lineStart: number; + + /** + * Current position in the string. + */ + pos: number; + + /** + * Where the stream's position was when it was first passed to the token function. + */ + start: number; + + /** + * The current line's content. + */ + string: string; + + /** + * Number of spaces per tab character. + */ + tabSize: number; + + /** + * Returns true only if the stream is at the end of the line. + */ + eol(): boolean; + + /** + * Returns true only if the stream is at the start of the line. + */ + sol(): boolean; + + /** + * Returns the next character in the stream without advancing it. Will return an null at the end of the line. + */ + peek(): string; + + /** + * Returns the next character in the stream and advances it. Also returns null when no more characters are available. + */ + next(): string; + + /** + * match can be a character, a regular expression, or a function that takes a character and returns a boolean. + * If the next character in the stream 'matches' the given argument, it is consumed and returned. + * Otherwise, undefined is returned. + */ + eat(match: string): string; + eat(match: RegExp): string; + eat(match: (char: string) => boolean): string; + + /** + * Repeatedly calls eat with the given argument, until it fails. Returns true if any characters were eaten. + */ + eatWhile(match: string): boolean; + eatWhile(match: RegExp): boolean; + eatWhile(match: (char: string) => boolean): boolean; + + /** + * Shortcut for eatWhile when matching white-space. + */ + eatSpace(): boolean; + + /** + * Moves the position to the end of the line. + */ + skipToEnd(): void; + + /** + * Skips to the next occurrence of the given character, if found on the current line (doesn't advance the stream if + * the character does not occur on the line). + * + * Returns true if the character was found. + */ + skipTo(ch: string): boolean; + + /** + * Act like a multi-character eat - if consume is true or not given - or a look-ahead that doesn't update the stream + * position - if it is false. pattern can be either a string or a regular expression starting with ^. When it is a + * string, caseFold can be set to true to make the match case-insensitive. When successfully matching a regular + * expression, the returned value will be the array returned by match, in case you need to extract matched groups. + */ + match(pattern: string, consume?: boolean, caseFold?: boolean): boolean; + match(pattern: RegExp, consume?: boolean): string[]; + + /** + * Backs up the stream n characters. Backing it up further than the start of the current token will cause things to + * break, so be careful. + */ + backUp(n: number): void; + + /** + * Returns the column (taking into account tabs) at which the current token starts. + */ + column(): number; + + /** + * Tells you how far the current line has been indented, in spaces. Corrects for tab characters. + */ + indentation(): number; + + /** + * Get the string between the start of the current token and the current stream position. + */ + current(): string; + } + + /** + * A Mode is, in the simplest case, a lexer (tokenizer) for your language — a function that takes a character stream as input, + * advances it past a token, and returns a style for that token. More advanced modes can also handle indentation for the language. + */ + interface Mode { + /** + * This function should read one token from the stream it is given as an argument, optionally update its state, + * and return a style string, or null for tokens that do not have to be styled. Multiple styles can be returned, separated by spaces. + */ + token(stream: StringStream, state: T): string; + + /** + * A function that produces a state object to be used at the start of a document. + */ + startState?: () => T; + /** + * For languages that have significant blank lines, you can define a blankLine(state) method on your mode that will get called + * whenever a blank line is passed over, so that it can update the parser state. + */ + blankLine?: (state: T) => void; + /** + * Given a state returns a safe copy of that state. + */ + copyState?: (state: T) => T; + + /** + * The indentation method should inspect the given state object, and optionally the textAfter string, which contains the text on + * the line that is being indented, and return an integer, the amount of spaces to indent. + */ + indent?: (state: T, textAfter: string) => number; + + /** The four below strings are used for working with the commenting addon. */ + /** + * String that starts a line comment. + */ + lineComment?: string; + /** + * String that starts a block comment. + */ + blockCommentStart?: string; + /** + * String that ends a block comment. + */ + blockCommentEnd?: string; + /** + * String to put at the start of continued lines in a block comment. + */ + blockCommentLead?: string; + + /** + * Trigger a reindent whenever one of the characters in the string is typed. + */ + electricChars?: string + /** + * Trigger a reindent whenever the regex matches the part of the line before the cursor. + */ + electricinput?: RegExp + } + + /** + * A function that, given a CodeMirror configuration object and an optional mode configuration object, returns a mode object. + */ + interface ModeFactory { + (config: CodeMirror.EditorConfiguration, modeOptions?: any): Mode + } + + /** + * id will be the id for the defined mode. Typically, you should use this second argument to defineMode as your module scope function + * (modes should not leak anything into the global scope!), i.e. write your whole mode inside this function. + */ + function defineMode(id: string, modefactory: ModeFactory): void; + + /** + * id will be the id for the defined mode. Typically, you should use this second argument to defineMode as your module scope function + * (modes should not leak anything into the global scope!), i.e. write your whole mode inside this function. + */ + function defineMode(id: string, modefactory: ModeFactory): void; + + /** + * The first argument is a configuration object as passed to the mode constructor function, and the second argument + * is a mode specification as in the EditorConfiguration mode option. + */ + function getMode(config: CodeMirror.EditorConfiguration, mode: any): Mode; + + /** + * Utility function from the overlay.js addon that allows modes to be combined. The mode given as the base argument takes care of + * most of the normal mode functionality, but a second (typically simple) mode is used, which can override the style of text. + * Both modes get to parse all of the text, but when both assign a non-null style to a piece of code, the overlay wins, unless + * the combine argument was true and not overridden, or state.overlay.combineTokens was true, in which case the styles are combined. + */ + function overlayMode(base: Mode, overlay: Mode, combine?: boolean): Mode + + /** + * async specifies that the lint process runs asynchronously. hasGutters specifies that lint errors should be displayed in the CodeMirror + * gutter, note that you must use this in conjunction with [ "CodeMirror-lint-markers" ] as an element in the gutters argument on + * initialization of the CodeMirror instance. + */ + interface LintStateOptions { + async: boolean; + hasGutters: boolean; + } + + /** + * Adds the getAnnotations callback to LintStateOptions which may be overridden by the user if they choose use their own + * linter. + */ + interface LintOptions extends LintStateOptions { + getAnnotations: AnnotationsCallback; + } + + /** + * A function that calls the updateLintingCallback with any errors found during the linting process. + */ + interface AnnotationsCallback { + (content: string, updateLintingCallback: UpdateLintingCallback, options: LintStateOptions, codeMirror: Editor): void; + } + + /** + * A function that, given an array of annotations, updates the CodeMirror linting GUI with those annotations + */ + interface UpdateLintingCallback { + (codeMirror: Editor, annotations: Annotation[]): void; + } + + /** + * An annotation contains a description of a lint error, detailing the location of the error within the code, the severity of the error, + * and an explaination as to why the error was thrown. + */ + interface Annotation { + from: Position; + message?: string; + severity?: string; + to?: Position; + } + + /** + * A function that calculates either a two-way or three-way merge between different sets of content. + */ + function MergeView(element: HTMLElement, options?: MergeView.MergeViewEditorConfiguration): MergeView.MergeViewEditor; + + namespace MergeView { + /** + * Options available to MergeView. + */ + interface MergeViewEditorConfiguration extends EditorConfiguration { + /** + * Determines whether the original editor allows editing. Defaults to false. + */ + allowEditingOriginals?: boolean; + + /** + * When true stretches of unchanged text will be collapsed. When a number is given, this indicates the amount + * of lines to leave visible around such stretches (which defaults to 2). Defaults to false. + */ + collapseIdentical?: boolean | number; + + /** + * Sets the style used to connect changed chunks of code. By default, connectors are drawn. When this is set to "align", + * the smaller chunk is padded to align with the bigger chunk instead. + */ + connect?: string; + + /** + * Callback for when stretches of unchanged text are collapsed. + */ + onCollapse?(mergeView: MergeViewEditor, line: number, size: number, mark: TextMarker): void; + + /** + * Provides original version of the document to be shown on the right of the editor. + */ + orig: any; + + /** + * Provides original version of the document to be shown on the left of the editor. + * To create a 2-way (as opposed to 3-way) merge view, provide only one of origLeft and origRight. + */ + origLeft?: any; + + /** + * Provides original version of document to be shown on the right of the editor. + * To create a 2-way (as opposed to 3-way) merge view, provide only one of origLeft and origRight. + */ + origRight?: any; + + /** + * Determines whether buttons that allow the user to revert changes are shown. Defaults to true. + */ + revertButtons?: boolean; + + /** + * When true, changed pieces of text are highlighted. Defaults to true. + */ + showDifferences?: boolean; + } + + interface MergeViewEditor extends Editor { + /** + * Returns the editor instance. + */ + editor(): Editor; + + /** + * Left side of the merge view. + */ + left: DiffView; + leftChunks(): MergeViewDiffChunk; + leftOriginal(): Editor; + + /** + * Right side of the merge view. + */ + right: DiffView; + rightChunks(): MergeViewDiffChunk; + rightOriginal(): Editor; + + /** + * Sets whether or not the merge view should show the differences between the editor views. + */ + setShowDifferences(showDifferences: boolean): void; + } + + /** + * Tracks changes in chunks from oroginal to new. + */ + interface MergeViewDiffChunk { + editFrom: number; + editTo: number; + origFrom: number; + origTo: number; + } + + interface DiffView { + /** + * Forces the view to reload. + */ + forceUpdate(): (mode: string) => void; + + /** + * Sets whether or not the merge view should show the differences between the editor views. + */ + setShowDifferences(showDifferences: boolean): void; + } + } + + var commands:{[name:string]:(editor:Editor) => void} + + + // Extension typings addon/scroll/annotatescrollbar.js + namespace AnnotateScrollbar { + interface Options { + className?: string; + scrollButtonHeight?: number; + listenForChanges?: boolean; + + } + + class Annotation { + constructor(cm:Editor, options:Options) + + computeScale():boolean + update(annotation:Range[]) + redraw(compute:boolean) + clear() + } + } + interface Editor { + annotateScrollbar(options:AnnotateScrollbar.Options):AnnotateScrollbar.Annotation + } +} + +declare module "codemirror" { + export = CodeMirror; +} diff --git a/typings/commonmark/commonmark.d.ts b/typings/commonmark/commonmark.d.ts new file mode 100644 index 000000000..84a76c47f --- /dev/null +++ b/typings/commonmark/commonmark.d.ts @@ -0,0 +1,214 @@ +// Type definitions for commonmark.js 0.22.1 +// Project: https://github.com/jgm/commonmark.js +// Definitions by: Nico Jansen +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + + +declare namespace commonmark { + + export interface NodeWalkingStep { + /** + * a boolean, which is true when we enter a Node from a parent or sibling, and false when we reenter it from a child + */ + entering: boolean; + /** + * The node belonging to this step + */ + node: Node; + } + + export interface NodeWalker { + /** + * Returns an object with properties entering and node. Returns null when we have finished walking the tree. + */ + next(): NodeWalkingStep; + /** + * Resets the iterator to resume at the specified node and setting for entering. (Normally this isn't needed unless you do destructive updates to the Node tree.) + */ + resumeAt(node: Node, entering?: boolean): void; + } + + export interface Position extends Array> { + } + + export interface ListData { + type?: string, + tight?: boolean, + delimiter?: string, + bulletChar?: string + } + + export class Node { + constructor(nodeType: string, sourcepos?: Position); + isContainer: boolean; + + /** + * (read-only): one of Text, Softbreak, Hardbreak, Emph, Strong, Html, Link, Image, Code, Document, Paragraph, BlockQuote, Item, List, Heading, CodeBlock, HtmlBlock ThematicBreak. + */ + type: string; + /** + * (read-only): a Node or null. + */ + firstChild: Node; + /** + * (read-only): a Node or null. + */ + lastChild: Node; + /** + * (read-only): a Node or null. + */ + next: Node; + /** + * (read-only): a Node or null. + */ + prev: Node; + /** + * (read-only): a Node or null. + */ + parent: Node; + /** + * (read-only): an Array with the following form: [[startline, startcolumn], [endline, endcolumn]] + */ + sourcepos: Position; + /** + * the literal String content of the node or null. + */ + literal: string; + /** + * link or image destination (String) or null. + */ + destination: string; + /** + * link or image title (String) or null. + */ + title: string; + /** + * fenced code block info string (String) or null. + */ + info: string; + /** + * heading level (Number). + */ + level: number; + /** + * either Bullet or Ordered (or undefined). + */ + listType: string; + /** + * true if list is tight + */ + listTight: boolean; + /** + * a Number, the starting number of an ordered list. + */ + listStart: number; + /** + * a String, either ) or . for an ordered list. + */ + listDelimiter: string; + /** + * used only for CustomBlock or CustomInline. + */ + onEnter: string; + /** + * used only for CustomBlock or CustomInline. + */ + onExit: string; + /** + * Append a Node child to the end of the Node's children. + */ + appendChild(child: Node): void; + /** + * Prepend a Node child to the beginning of the Node's children. + */ + prependChild(child: Node): void; + /** + * Remove the Node from the tree, severing its links with siblings and parents, and closing up gaps as needed. + */ + unlink(): void; + /** + * Insert a Node sibling after the Node. + */ + insertAfter(sibling: Node): void; + /** + * Insert a Node sibling before the Node. + */ + insertBefore(sibling: Node): void; + /** + * Returns a NodeWalker that can be used to iterate through the Node tree rooted in the Node + */ + walker(): NodeWalker; + /** + * Setting the backing object of listType, listTight, listStat and listDelimiter directly. + * Not needed unless creating list nodes directly. Should be fixed from v>0.22.1 + * https://github.com/jgm/commonmark.js/issues/74 + */ + _listData: ListData; + } + + /** + * Instead of converting Markdown directly to HTML, as most converters do, commonmark.js parses Markdown to an AST (abstract syntax tree), and then renders this AST as HTML. + * This opens up the possibility of manipulating the AST between parsing and rendering. For example, one could transform emphasis into ALL CAPS. + */ + export class Parser { + /** + * Constructs a new Parser + */ + constructor(options?: ParserOptions); + parse(input: string): Node; + } + + export interface ParserOptions { + /** + * if true, straight quotes will be made curly, -- will be changed to an en dash, --- will be changed to an em dash, and ... will be changed to ellipses. + */ + smart?: boolean; + time?: boolean; + } + + export interface HtmlRenderingOptions extends XmlRenderingOptions { + /** + * if true, raw HTML will not be passed through to HTML output (it will be replaced by comments), and potentially unsafe URLs in links and images (those beginning with javascript:, vbscript:, file:, and with a few exceptions data:) will be replaced with empty strings. + */ + safe?: boolean; + /** + * if true, straight quotes will be made curly, -- will be changed to an en dash, --- will be changed to an em dash, and ... will be changed to ellipses. + */ + smart?: boolean; + /** + * if true, source position information for block-level elements will be rendered in the data-sourcepos attribute (for HTML) or the sourcepos attribute (for XML). + */ + sourcepos?: boolean; + } + + export class HtmlRenderer { + constructor(options?: HtmlRenderingOptions) + render(root: Node): string; + /** + * Let's you override the softbreak properties of a renderer. So, to make soft breaks render as hard breaks in HTML: + * writer.softbreak = "
"; + */ + softbreak: string; + /** + * Override the function that will be used to escape (sanitize) the html output. Return value is used to add to the html output + * @param input the input to escape + * @param isAttributeValue indicates wheter or not the input value will be used as value of an html attribute. + */ + escape: (input: string, isAttributeValue: boolean) => string; + } + + export interface XmlRenderingOptions { + time?: boolean; + sourcepos?: boolean; + } + + export class XmlRenderer { + constructor(options?: XmlRenderingOptions) + render(root: Node): string; + } + +} + +declare module 'commonmark' { + export = commonmark; +}