diff --git a/.gitignore b/.gitignore index d9148e9..102c661 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ pom.xml *jar lib -classes \ No newline at end of file +classes +*~ +.lein-deps-sum diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9cf7391 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "resources/public/javascript/jquery-console"] + path = resources/public/javascript/jquery-console + url = https://github.com/chrisdone/jquery-console.git diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4f42080 --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: clojure \ No newline at end of file diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..92abe1c --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: java -Djava.security.policy=example.policy $JVM_OPTS -cp target/tryclojure-standalone.jar clojure.main -m tryclojure.server $PORT diff --git a/README b/README deleted file mode 100644 index 57f30c5..0000000 --- a/README +++ /dev/null @@ -1,13 +0,0 @@ -# tryclojure - -TryClojure is a online Clojure REPL written using Moustache, Ring, and Chris Done's jquery console (you're awesome, Chris). - -## Usage - -http://www.try-clojure.org - -Many thanks to Licenser for giving me a place to put it. - -## License - -Licensed under the same thing Clojure is licensed under: the EPL, of which you can find a copy at the root of this directory. diff --git a/README.md b/README.md new file mode 100644 index 0000000..65cfeb7 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# tryclojure + +TryClojure is a online Clojure REPL written using Noir and Chris Done's jquery console (you're awesome, Chris). + +[](http://travis-ci.org/Raynes/tryclojure) + +## Usage + +http://tryclj.com + +To run it locally, use `lein ring server`. + +## Credits + +apgwoz: Design + +## License + +Licensed under the same thing Clojure is licensed under: the EPL, of which you can find a copy at the root of this directory. diff --git a/example.policy b/example.policy new file mode 100644 index 0000000..dc85222 --- /dev/null +++ b/example.policy @@ -0,0 +1,3 @@ +grant { + permission java.security.AllPermission; +}; diff --git a/project.clj b/project.clj index 5b90c5d..44808a1 100644 --- a/project.clj +++ b/project.clj @@ -1,12 +1,13 @@ (defproject tryclojure "0.1.0-SNAPSHOT" :description "A simple web-based Clojure REPL for trying out Clojure without having to install it." - :dependencies [[org.clojure/clojure "1.1.0"] - [org.clojure/clojure-contrib "1.1.0"] - [net.cgrand/moustache "1.0.0-SNAPSHOT"] - [ring "0.2.5"] - [commons-lang/commons-lang "2.5"] - [clj-sandbox "0.3.8"] - [clj-highlight "0.1.1-SNAPSHOT"] - [hiccup "0.2.6"]] - :dev-dependencies [[swank-clojure "1.2.1"] - [ring/ring-devel "0.2.5"]]) + :dependencies [[org.clojure/clojure "1.4.0"] + [lib-noir "0.8.1"] + [compojure "1.1.6"] + [ring-server "0.3.1"] + [commons-lang/commons-lang "2.5"] + [clojail "1.0.6"]] + :jvm-opts ["-Djava.security.policy=example.policy" "-Xmx80M"] + :min-lein-version "2.0.0" + :uberjar-name "tryclojure-standalone.jar" + :plugins [[lein-ring "0.8.10"]] + :ring {:handler tryclojure.server/app :port 8801}) diff --git a/resources/public/clojure-logo.png b/resources/public/clojure-logo.png new file mode 100644 index 0000000..583ebce Binary files /dev/null and b/resources/public/clojure-logo.png differ diff --git a/resources/public/css/gh-fork-ribbon.css b/resources/public/css/gh-fork-ribbon.css new file mode 100644 index 0000000..6fe4efb --- /dev/null +++ b/resources/public/css/gh-fork-ribbon.css @@ -0,0 +1,82 @@ +/* From https://github.com/simonwhitaker/github-fork-ribbon-css */ + +/* Left will inherit from right (so we don't need to duplicate code */ +.github-fork-ribbon { + /* The right and left lasses determine the side we attach our banner to */ + position: absolute; + + /* Add a bit of padding to give some substance outside the "stitching" */ + padding: 2px 0; + + /* Set the base colour */ + background-color: #5881d8; + + /* Set a gradient: transparent black at the top to almost-transparent black at the bottom */ + background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0.00)), to(rgba(0, 0, 0, 0.15))); + background-image: -webkit-linear-gradient(top, rgba(0, 0, 0, 0.00), rgba(0, 0, 0, 0.15)); + background-image: -moz-linear-gradient(top, rgba(0, 0, 0, 0.00), rgba(0, 0, 0, 0.15)); + background-image: -o-linear-gradient(top, rgba(0, 0, 0, 0.00), rgba(0, 0, 0, 0.15)); + background-image: -ms-linear-gradient(top, rgba(0, 0, 0, 0.00), rgba(0, 0, 0, 0.15)); + background-image: linear-gradient(top, rgba(0, 0, 0, 0.00), rgba(0, 0, 0, 0.15)); + filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#000000', EndColorStr='#000000'); + + /* Add a drop shadow */ + -webkit-box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.5); + box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.5); +} +.github-fork-ribbon a { + /* Set the font */ + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + font-weight: 700; + color: white; + + /* Set the text properties */ + text-decoration: none; + text-shadow: 0 -1px rgba(0,0,0,0.5); + text-align: center; + + /* Set the geometry. If you fiddle with these you'll also need to tweak the top and right values in #github-fork-ribbon. */ + width: 200px; + line-height: 20px; + + /* Set the layout properties */ + display: inline-block; + padding: 2px 0; + + /* Add "stitching" effect */ + border-width: 1px 0; + border-style: dotted; + border-color: rgba(255,255,255,0.7); +} + +.github-fork-ribbon-wrapper { + width: 150px; + height: 150px; + position: absolute; + overflow: hidden; + top: 0; +} + +.github-fork-ribbon-wrapper.right { + right: 0; +} + +.github-fork-ribbon-wrapper.right .github-fork-ribbon { + top: 42px; + right: -43px; + + /* Rotate the banner 45 degrees */ + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -o-transform: rotate(45deg); + transform: rotate(45deg); +} + +@media (max-width: 619px) { + + .github-fork-ribbon { + zoom: 0.6; + } + +} diff --git a/resources/public/css/tryclojure.css b/resources/public/css/tryclojure.css index 9d03de9..e5145d8 100644 --- a/resources/public/css/tryclojure.css +++ b/resources/public/css/tryclojure.css @@ -1,63 +1,172 @@ -body { background-color: white; } -div#container { width: 600px; height: 100%; - margin: 50px auto 50px auto; background-color: white;} -h1 { font-size: 32pt; color: black; border-bottom: 1px solid #333; font-family: arial;} -.bottom { background-color: white; color: #333; } -.bottom a,a:visited { color: #111; } -table.bottom { width: 100%; border: 1px solid black;} -div#changer { width: 100%; height: 100%; } -div.buttons { background: transparent url('/resources/public/buttontdbg.png') no-repeat top left; - padding-left: 0.2em; padding-right: 0.2em; text-align: center; } -.buttons a.buttons { border-right: 1px solid #5F665F; color: black; padding-right: 0.3em; - font-family: verdana; font-size: 10pt; font-weight: bold; padding-left: 0.3em;} -a.lbutton { color: black; font-family: verdana; font-size: 10pt; font-weight: bold; - padding-left: 0.3em; padding-right: 0.3em; } -td.bholder { border: 1px solid black; } -div#tuttext { overflow-y: auto; max-height: 300px; } -a.buttons:hover,a.lbutton:hover { color: white; cursor: pointer; } -div.continue { width: 100%; text-align: center; padding-top: 0.5em; } - -p.bottom -{ - padding: 0; - font-family: verdana; - font-size: 10pt; -} -p -{ - color:gray; - font-family:\"Times New Roman\"; - font-size:14px; -} -div.bottom { width: 600px; } -div.footer { width: 100%; border-top: 1px solid black; text-align: center; } -div.console div.jquery-console-inner -{ width:595px; height:300px; background:#efefef; padding-left: 3px; padding-top: 3px; - overflow:auto; text-align:left; border: 1px solid black; - background: transparent url('/resources/public/wtf.png') no-repeat top left; } -div.console div.jquery-console-message-value -{ color:#0066FF; font-family:monospace; - padding:0.1em; } -div.console div.jquery-console-prompt-box -{ color:#444; font-family:monospace; } -div.console div.jquery-console-focus span.jquery-console-cursor -{ background:#333; color:#eee; font-weight:bold } -div.console div.jquery-console-message-error -{ color:#ef0505; font-family:sans-serif; font-weight:bold; - padding:0.1em; } -div.console div.jquery-console-message-success -{ color:#187718; font-family:monospace; - padding:0.1em; } -div.console span.jquery-console-prompt-label { font-weight:bold } +body { + background: #F7F7F7; + text-align: center; + font-family: Helvetica, sans-serif; + color: #6C7B89; +} + +#wrapper { + text-align: left; + width: 620px; + margin: 0 auto; +} + +#header { + margin: 20px 0px 20px 190px; +} + +#header h1 { + height: 72px; + line-height: 72px; + background: url(../clojure-logo.png) no-repeat; + padding: 0px 0px 0px 80px; + font-weight: normal; + font-size: 36px; +} + +#header h1 span.logo-try { + color: #63b132; + display:inline; +} + +#header h1 span.logo-clojure { + color: #2B4A8B; + display:inline +} + +#container { + margin-bottom: 40px; +} + +#console { + height: 220px; + padding: 10px; + background: #FFFFFF; + font-family: monospace; + font-size: 14px; + color: #6C7B89; + margin-bottom: 10px; +} + +#console textarea { + /* Use a largish font to ensure iOS doesn't zoom in on focus */ + font-size: 2em; +} + +#console div.jquery-console-inner { + width:580px; + height:200px; + margin: 10px 10px; + overflow:auto; + text-align:left; +} + +#console div.jquery-console-message-value { + color:#63b132; +} + +#console div.jquery-console-prompt-box { + color: #6C7B89; +} + +#console div.jquery-console-focus span.jquery-console-cursor { + background:#6C7B89; + color: #FFFFFF; +} + +#console div.jquery-console-message-error { + color:#FD713B; +} + +#console div.jquery-console-message-success { + color:#63b132; +} + +#console span.jquery-console-prompt-label { + font-weight:bold; +} + +#buttons { + background: transparent; + text-align: center; + margin-bottom: 10px; +} + +#buttons a { + display: inline-block; + color: #2B4A8B; + padding: 2px 2px; + border-bottom: 2px solid #2B4A8B; + text-transform: uppercase; + margin: 10px; + font-family: Helvetica, sans-serif; + font-size: 16px; + transition: all 100ms; +} + +#buttons a.last { + margin-right: 0; +} + +#buttons a:hover { + cursor: pointer; +} + +#changer { + line-height: 1.3em; + padding: 20px; + background: #D6DADD; + margin-bottom: 10px; +} + +#changer p { + margin: 0.6em; +} + +#changer a { + color: #2B4A8B; +} + +#changer a:visited { + color: #60749E; +} + +#changer code { + background-color: #eee; + border: 1px solid #aaa; + color: #555; + font-family: courier, monospace; + padding: 0.1em 0.25em 0.1em 0.25em; +} + +#changer ul { + margin: 0px; + padding: 0px; + list-style: none; +} + +#tuttext { +} + +.continue { + width: 100%; + text-align: center; + padding-top: 0.5em; +} + +.footer { + text-align: center; +} /* Coderay alpha style */ +code { + display: inline-block; +} .code { - background-color: #f8f8f8; - border: 1px solid silver; -/* font-family: 'Courier New', 'Terminal', monospace; */ color: #000; } + .code pre { margin: 0px; } span.code { white-space: pre; border: 0px; padding: 2px; } @@ -71,105 +180,54 @@ table.code td { padding: 2px 4px; vertical-align: top; } color: gray; text-align: right; } -.code .line_numbers a:target, .code .no a:target { color: blue; } -.code .line_numbers .highlighted, .code .no .highlighted { color: red; } -.code .no { padding: 0px 4px; } -.code .code { width: 100%; } -.code .code pre { overflow: auto; } - -.debug { color:white ! important; background:blue ! important; } - -.an { color:#007 } -.at { color:#f08 } -.av { color:#700 } -.bi { color:#509; font-weight:bold } -.c { color:#888; } -.c .dl { color:#444; } -.c .ch { color:#444; } - -.ch { color:#04D } -.ch .k { color:#04D } -.ch .dl { color:#039 } - -.cl { color:#B06; font-weight:bold } -.cm { color:#A08; font-weight:bold } -.co { color:#036; font-weight:bold } -.cr { color:#0A0 } -.cv { color:#369 } -.de { color:#B0B; } -.df { color:#099; font-weight:bold } -.di { color:#088; font-weight:bold } -.dl { color:black } -.do { color:#970 } -.dt { color:#34b } -.ds { color:#D42; font-weight:bold } -.e { color:#666; font-weight:bold } -.en { color:#800; font-weight:bold } -.er { color:#F00; background-color:#FAA } -.ex { color:#C00; font-weight:bold } -.fl { color:#60E; font-weight:bold } -.fu { color:#06B; font-weight:bold } -.gv { color:#d70; font-weight:bold } -.hx { color:#058; font-weight:bold } -.i { color:#00D; font-weight:bold } -.ic { color:#B44; font-weight:bold } - -.il { background-color: hsla(0,0%,0%,0.1); color: black } -.il .idl { font-weight: bold; color: #666 } -.idl { font-weight: bold; color: #666; } - -.im { color:#f00; } -.in { color:#B2B; font-weight:bold } -.iv { color:#33B } -.la { color:#970; font-weight:bold } -.lv { color:#963 } -.ns { color:#707; font-weight:bold } -.oc { color:#40E; font-weight:bold } -.op { } -.pc { color:#058; font-weight:bold } -.pd { color:#369; font-weight:bold } -.pp { color:#579; } -.ps { color:#00C; font-weight:bold } -.pt { color:#074; font-weight:bold } -.r, .kw { color:#080; font-weight:bold } - -.ke { color: #808; } -.ke .dl { color: #606; } -.ke .ch { color: #80f; } -.vl { color: #088; } - -.rx { background-color:hsla(300,100%,50%,0.1); color:#808 } -.rx .k { } -.rx .dl { color:#404 } -.rx .mod { color:#C2C } -.rx .fu { color:#404; font-weight: bold } - -.s { color: #D20; } -.s .k { } -.s .ch { color: #b0b; } -.s .dl { color: #710; } - -.sh { background-color:hsla(120,100%,50%,0.1); color:#2B2 } -.sh .k { } -.sh .dl { color:#161 } - -.sy { color:#A60 } -.sy .k { color:#A60 } -.sy .dl { color:#630 } - -.ta { color:#070 } -.ts { color:#D70; font-weight:bold } -.ty { color:#339; font-weight:bold } -.v { color:#036 } -.xt { color:#444 } - -.ins { background: #afa; } -.del { background: #faa; } -.chg { color: #aaf; background: #007; } -.head { color: #f8f; background: #505 } -.head .filename { color: white; } - -.ins .ins { color: #080; font-weight:bold } -.del .del { color: #800; font-weight:bold } -.chg .chg { color: #66f; } -.head .head { color: #f4f; } \ No newline at end of file + +/* keywords */ +#changer .code span.r { + color: #0000ff; + font-weight: bold; +} + +/* symbols */ +#changer .code span.sy { + color: #318495; +} + +/* strings */ +#changer .code span.s { + color: #008800; +} + +/* paren */ +#changer .code span.of { + color: #222; + font-weight: bold; +} + +/* comment */ +#changer .code span.c { + color: #ccc; +} + +/* operator */ +#changer .code span.cl { + +} + +/* number */ +#changer .code span.i { + color: #ff0000; +} + +@media (max-width: 619px) { + + #wrapper { + width: 100%; + } + #header { + margin: 20px auto; + } + #console div.jquery-console-inner { + width: auto; + } + +} diff --git a/resources/public/javascript/jquery-console/.gitignore b/resources/public/javascript/jquery-console/.gitignore new file mode 100644 index 0000000..ac8f968 --- /dev/null +++ b/resources/public/javascript/jquery-console/.gitignore @@ -0,0 +1,20 @@ +# Compiled source # +################### +*.com +*.class +*.dll +*.exe +*.o +*.so +*.pyc + +# Logs and databases # +###################### +*.log + +# OS generated files # +###################### +.DS_Store* +ehthumbs.db +Icon? +Thumbs.db diff --git a/resources/public/javascript/jquery-console/README b/resources/public/javascript/jquery-console/README new file mode 100644 index 0000000..c5d2de3 --- /dev/null +++ b/resources/public/javascript/jquery-console/README @@ -0,0 +1,81 @@ +INTRODUCTION + +See demo.html. Or here: http://chrisdone.com/jquery-console/ + +Options available: + + autofocus bool Autofocus the terminal, rather than + having to click on it. + + promptHistory bool Provide history support (kind of crappy, + needs doing properly.) + + historyPreserveColumn bool Preserve the column you were one when + switching between history. + + welcomeMessage string Just a first message to display on the + terminal. + + promptLabel string Prompt string like 'JavaScript> '. + + cols integer the number of cols, this value is only + used by the command completion to format + the list of results. + + commandValidate function When user hits return, validate + whether to trigger commandHandle and + re-prompt. + + commandHandle function Handle the command line, return a + string, boolean, or list + of {msg:"foo",className:"my-css-class"}. + commandHandle(line,report) is + called. Report function is for you + to report a result of the command + asynchronously. + + commandComplete function Handle the command completion when the + tab key is pressed. It returns a list + of string completion suffixes. + + animateScroll bool Whether to animate the scroll to + top. Currently disabled. + + charInsertTrigger function Predicate for whether to allow + character insertion. + charInsertTrigger(char,line) is called. + + cancelHandle function Handle a user-signaled interrupt. + + fadeOnReset bool Whether to trigger a fade in/out when + the console is reset. Defaults to true. + +LICENSE + +Copyright 2010 Chris Done, Simon David Pratt. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + 1. Redistributions of source code must retain the above + copyright notice, this list of conditions and the following + disclaimer. + + 2. Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials + provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/resources/public/javascript/jquery-console/demo.html b/resources/public/javascript/jquery-console/demo.html new file mode 100644 index 0000000..7f85186 --- /dev/null +++ b/resources/public/javascript/jquery-console/demo.html @@ -0,0 +1,221 @@ + + + + JQuery Console Demo + + + + + + + + + +

Simple console demo

+

Tested on:

+ + + diff --git a/resources/public/javascript/jquery-1.4.2.min.js b/resources/public/javascript/jquery-console/jquery-1.4.2.min.js similarity index 100% rename from resources/public/javascript/jquery-1.4.2.min.js rename to resources/public/javascript/jquery-console/jquery-1.4.2.min.js diff --git a/resources/public/javascript/jquery-console/jquery.console.js b/resources/public/javascript/jquery-console/jquery.console.js new file mode 100644 index 0000000..5c1ac3f --- /dev/null +++ b/resources/public/javascript/jquery-console/jquery.console.js @@ -0,0 +1,755 @@ +// JQuery Console 1.0 +// Sun Feb 21 20:28:47 GMT 2010 +// +// Copyright 2010 Chris Done, Simon David Pratt. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// 1. Redistributions of source code must retain the above +// copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials +// provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +// COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +// TESTED ON +// Internet Explorer 6 +// Opera 10.01 +// Chromium 4.0.237.0 (Ubuntu build 31094) +// Firefox 3.5.8, 3.6.2 (Mac) +// Safari 4.0.5 (6531.22.7) (Mac) +// Google Chrome 5.0.375.55 (Mac) + +(function($){ + var isWebkit = !!~navigator.userAgent.indexOf(' AppleWebKit/'); + + $.fn.console = function(config){ + //////////////////////////////////////////////////////////////////////// + // Constants + // Some are enums, data types, others just for optimisation + var keyCodes = { + // left + 37: moveBackward, + // right + 39: moveForward, + // up + 38: previousHistory, + // down + 40: nextHistory, + // backspace + 8: backDelete, + // delete + 46: forwardDelete, + // end + 35: moveToEnd, + // start + 36: moveToStart, + // return + 13: commandTrigger, + // tab + 18: doNothing, + // tab + 9: doComplete + }; + var ctrlCodes = { + // C-a + 65: moveToStart, + // C-e + 69: moveToEnd, + // C-d + 68: forwardDelete, + // C-n + 78: nextHistory, + // C-p + 80: previousHistory, + // C-b + 66: moveBackward, + // C-f + 70: moveForward, + // C-k + 75: deleteUntilEnd + }; + if(config.ctrlCodes) { + $.extend(ctrlCodes, config.ctrlCodes); + } + var altCodes = { + // M-f + 70: moveToNextWord, + // M-b + 66: moveToPreviousWord, + // M-d + 68: deleteNextWord + }; + var shiftCodes = { + // return + 13: newLine, + }; + var cursor = ' '; + + //////////////////////////////////////////////////////////////////////// + // Globals + var container = $(this); + var inner = $('
'); + // erjiang: changed this from a text input to a textarea so we + // can get pasted newlines + var typer = $(''); + // Prompt + var promptBox; + var prompt; + var promptLabel = config && config.promptLabel? config.promptLabel : "> "; + var continuedPromptLabel = config && config.continuedPromptLabel? + config.continuedPromptLabel : "> "; + var column = 0; + var promptText = ''; + var restoreText = ''; + var continuedText = ''; + var fadeOnReset = config.fadeOnReset !== undefined ? config.fadeOnReset : true; + // Prompt history stack + var history = []; + var ringn = 0; + // For reasons unknown to The Sword of Michael himself, Opera + // triggers and sends a key character when you hit various + // keys like PgUp, End, etc. So there is no way of knowing + // when a user has typed '#' or End. My solution is in the + // typer.keydown and typer.keypress functions; I use the + // variable below to ignore the keypress event if the keydown + // event succeeds. + var cancelKeyPress = 0; + // When this value is false, the prompt will not respond to input + var acceptInput = true; + // When this value is true, the command has been canceled + var cancelCommand = false; + + // External exports object + var extern = {}; + + //////////////////////////////////////////////////////////////////////// + // Main entry point + (function(){ + container.append(inner); + inner.append(typer); + typer.css({position:'absolute',top:0,left:'-9999px'}); + if (config.welcomeMessage) + message(config.welcomeMessage,'jquery-console-welcome'); + newPromptBox(); + if (config.autofocus) { + inner.addClass('jquery-console-focus'); + typer.focus(); + setTimeout(function(){ + inner.addClass('jquery-console-focus'); + typer.focus(); + },100); + } + extern.inner = inner; + extern.typer = typer; + extern.scrollToBottom = scrollToBottom; + })(); + + //////////////////////////////////////////////////////////////////////// + // Reset terminal + extern.reset = function(){ + var welcome = (typeof config.welcomeMessage != 'undefined'); + + var removeElements = function() { + inner.find('div').each(function(){ + if (!welcome) { + $(this).remove(); + } else { + welcome = false; + } + }); + }; + + var focusConsole = function() { + inner.addClass('jquery-console-focus'); + typer.focus(); + }; + + if (fadeOnReset) { + inner.parent().fadeOut(function() { + removeElements(); + newPromptBox(); + inner.parent().fadeIn(focusConsole); + }); + } + else { + removeElements(); + newPromptBox(); + focusConsole(); + } + }; + + //////////////////////////////////////////////////////////////////////// + // Reset terminal + extern.notice = function(msg,style){ + var n = $('
').append($('
').text(msg)) + .css({visibility:'hidden'}); + container.append(n); + var focused = true; + if (style=='fadeout') + setTimeout(function(){ + n.fadeOut(function(){ + n.remove(); + }); + },4000); + else if (style=='prompt') { + var a = $('
OK
'); + n.append(a); + focused = false; + a.click(function(){ n.fadeOut(function(){ n.remove();inner.css({opacity:1}) }); }); + } + var h = n.height(); + n.css({height:'0px',visibility:'visible'}) + .animate({height:h+'px'},function(){ + if (!focused) inner.css({opacity:0.5}); + }); + n.css('cursor','default'); + return n; + }; + + //////////////////////////////////////////////////////////////////////// + // Make a new prompt box + function newPromptBox() { + column = 0; + promptText = ''; + ringn = 0; // Reset the position of the history ring + enableInput(); + promptBox = $('
'); + var label = $(''); + var labelText = extern.continuedPrompt? continuedPromptLabel : promptLabel; + promptBox.append(label.text(labelText).show()); + label.html(label.html().replace(' ',' ')); + prompt = $(''); + promptBox.append(prompt); + inner.append(promptBox); + updatePromptDisplay(); + }; + + //////////////////////////////////////////////////////////////////////// + // Handle setting focus + container.click(function(){ + // Don't mess with the focus if there is an active selection + if (window.getSelection().toString()) { + return false; + } + + inner.addClass('jquery-console-focus'); + inner.removeClass('jquery-console-nofocus'); + if (isWebkit) { + typer.focusWithoutScrolling(); + } else { + typer.css('position', 'fixed').focus(); + } + scrollToBottom(); + return false; + }); + + //////////////////////////////////////////////////////////////////////// + // Handle losing focus + typer.blur(function(){ + inner.removeClass('jquery-console-focus'); + inner.addClass('jquery-console-nofocus'); + }); + + //////////////////////////////////////////////////////////////////////// + // Bind to the paste event of the input box so we know when we + // get pasted data + typer.bind('paste', function(e) { + // wipe typer input clean just in case + typer.val(""); + // this timeout is required because the onpaste event is + // fired *before* the text is actually pasted + setTimeout(function() { + typer.consoleInsert(typer.val()); + typer.val(""); + }, 0); + }); + + //////////////////////////////////////////////////////////////////////// + // Handle key hit before translation + // For picking up control characters like up/left/down/right + + typer.keydown(function(e){ + cancelKeyPress = 0; + var keyCode = e.keyCode; + // C-c: cancel the execution + if(e.ctrlKey && keyCode == 67) { + cancelKeyPress = keyCode; + cancelExecution(); + return false; + } + if (acceptInput) { + if (e.shiftKey && keyCode in shiftCodes) { + cancelKeyPress = keyCode; + (shiftCodes[keyCode])(); + return false; + } else if (e.altKey && keyCode in altCodes) { + cancelKeyPress = keyCode; + (altCodes[keyCode])(); + return false; + } else if (e.ctrlKey && keyCode in ctrlCodes) { + cancelKeyPress = keyCode; + (ctrlCodes[keyCode])(); + return false; + } else if (keyCode in keyCodes) { + cancelKeyPress = keyCode; + (keyCodes[keyCode])(); + return false; + } + } + }); + + //////////////////////////////////////////////////////////////////////// + // Handle key press + typer.keypress(function(e){ + var keyCode = e.keyCode || e.which; + if (isIgnorableKey(e)) { + return false; + } + // C-v: don't insert on paste event + if ((e.ctrlKey || e.metaKey) && String.fromCharCode(keyCode).toLowerCase() == 'v') { + return true; + } + if (acceptInput && cancelKeyPress != keyCode && keyCode >= 32){ + if (cancelKeyPress) return false; + if ( + typeof config.charInsertTrigger == 'undefined' || ( + typeof config.charInsertTrigger == 'function' && + config.charInsertTrigger(keyCode,promptText) + ) + ){ + typer.consoleInsert(keyCode); + } + } + if (isWebkit) return false; + }); + + function isIgnorableKey(e) { + // for now just filter alt+tab that we receive on some platforms when + // user switches windows (goes away from the browser) + return ((e.keyCode == keyCodes.tab || e.keyCode == 192) && e.altKey); + }; + + //////////////////////////////////////////////////////////////////////// + // Rotate through the command history + function rotateHistory(n){ + if (history.length == 0) return; + ringn += n; + if (ringn < 0) ringn = history.length; + else if (ringn > history.length) ringn = 0; + var prevText = promptText; + if (ringn == 0) { + promptText = restoreText; + } else { + promptText = history[ringn - 1]; + } + if (config.historyPreserveColumn) { + if (promptText.length < column + 1) { + column = promptText.length; + } else if (column == 0) { + column = promptText.length; + } + } else { + column = promptText.length; + } + updatePromptDisplay(); + }; + + function previousHistory() { + rotateHistory(-1); + }; + + function nextHistory() { + rotateHistory(1); + }; + + // Add something to the history ring + function addToHistory(line){ + history.push(line); + restoreText = ''; + }; + + // Delete the character at the current position + function deleteCharAtPos(){ + if (column < promptText.length){ + promptText = + promptText.substring(0,column) + + promptText.substring(column+1); + restoreText = promptText; + return true; + } else return false; + }; + + function backDelete() { + if (moveColumn(-1)){ + deleteCharAtPos(); + updatePromptDisplay(); + } + }; + + function forwardDelete() { + if (deleteCharAtPos()){ + updatePromptDisplay(); + } + }; + + function deleteUntilEnd() { + while(deleteCharAtPos()) { + updatePromptDisplay(); + } + }; + + function deleteNextWord() { + // A word is defined within this context as a series of alphanumeric + // characters. + // Delete up to the next alphanumeric character + while( + column < promptText.length && + !isCharAlphanumeric(promptText[column]) + ) { + deleteCharAtPos(); + updatePromptDisplay(); + } + // Then, delete until the next non-alphanumeric character + while( + column < promptText.length && + isCharAlphanumeric(promptText[column]) + ) { + deleteCharAtPos(); + updatePromptDisplay(); + } + }; + + function newLine() { + promptText += "\n"; + moveColumn(1); + updatePromptDisplay(); + }; + + //////////////////////////////////////////////////////////////////////// + // Validate command and trigger it if valid, or show a validation error + function commandTrigger() { + var line = promptText; + if (typeof config.commandValidate == 'function') { + var ret = config.commandValidate(line); + if (ret == true || ret == false) { + if (ret) { + handleCommand(); + } + } else { + commandResult(ret,"jquery-console-message-error"); + } + } else { + handleCommand(); + } + }; + + // Scroll to the bottom of the view + function scrollToBottom() { + var version = jQuery.fn.jquery.split('.'); + var major = parseInt(version[0]); + var minor = parseInt(version[1]); + + // check if we're using jquery > 1.6 + if ((major == 1 && minor > 6) || major > 1) { + inner.prop({ scrollTop: inner.prop("scrollHeight") }); + } + else { + inner.attr({ scrollTop: inner.attr("scrollHeight") }); + } + }; + + function cancelExecution() { + if(typeof config.cancelHandle == 'function') { + config.cancelHandle(); + } + } + + //////////////////////////////////////////////////////////////////////// + // Handle a command + function handleCommand() { + if (typeof config.commandHandle == 'function') { + disableInput(); + addToHistory(promptText); + var text = promptText; + if (extern.continuedPrompt) { + if (continuedText) + continuedText += '\n' + promptText; + else continuedText = promptText; + } else continuedText = undefined; + if (continuedText) text = continuedText; + var ret = config.commandHandle(text,function(msgs){ + commandResult(msgs); + }); + if (extern.continuedPrompt && !continuedText) + continuedText = promptText; + if (typeof ret == 'boolean') { + if (ret) { + // Command succeeded without a result. + commandResult(); + } else { + commandResult( + 'Command failed.', + "jquery-console-message-error" + ); + } + } else if (typeof ret == "string") { + commandResult(ret,"jquery-console-message-success"); + } else if (typeof ret == 'object' && ret.length) { + commandResult(ret); + } else if (extern.continuedPrompt) { + commandResult(); + } + } + }; + + //////////////////////////////////////////////////////////////////////// + // Disable input + function disableInput() { + acceptInput = false; + }; + + // Enable input + function enableInput() { + acceptInput = true; + } + + //////////////////////////////////////////////////////////////////////// + // Reset the prompt in invalid command + function commandResult(msg,className) { + column = -1; + updatePromptDisplay(); + if (typeof msg == 'string') { + message(msg,className); + } else if ($.isArray(msg)) { + for (var x in msg) { + var ret = msg[x]; + message(ret.msg,ret.className); + } + } else { // Assume it's a DOM node or jQuery object. + inner.append(msg); + } + newPromptBox(); + }; + + //////////////////////////////////////////////////////////////////////// + // Display a message + function message(msg,className) { + var mesg = $('
'); + if (className) mesg.addClass(className); + mesg.filledText(msg).hide(); + inner.append(mesg); + mesg.show(); + }; + + //////////////////////////////////////////////////////////////////////// + // Handle normal character insertion + // data can either be a number, which will be interpreted as the + // numeric value of a single character, or a string + typer.consoleInsert = function(data){ + // TODO: remove redundant indirection + var text = isNaN(data) ? data : String.fromCharCode(data); + var before = promptText.substring(0,column); + var after = promptText.substring(column); + promptText = before + text + after; + moveColumn(text.length); + restoreText = promptText; + updatePromptDisplay(); + }; + + //////////////////////////////////////////////////////////////////////// + // Move to another column relative to this one + // Negative means go back, positive means go forward. + function moveColumn(n){ + if (column + n >= 0 && column + n <= promptText.length){ + column += n; + return true; + } else return false; + }; + + function moveForward() { + if(moveColumn(1)) { + updatePromptDisplay(); + return true; + } + return false; + }; + + function moveBackward() { + if(moveColumn(-1)) { + updatePromptDisplay(); + return true; + } + return false; + }; + + function moveToStart() { + if (moveColumn(-column)) + updatePromptDisplay(); + }; + + function moveToEnd() { + if (moveColumn(promptText.length-column)) + updatePromptDisplay(); + }; + + function moveToNextWord() { + while( + column < promptText.length && + !isCharAlphanumeric(promptText[column]) && + moveForward() + ) {} + while( + column < promptText.length && + isCharAlphanumeric(promptText[column]) && + moveForward() + ) {} + }; + + function moveToPreviousWord() { + // Move backward until we find the first alphanumeric + while( + column -1 >= 0 && + !isCharAlphanumeric(promptText[column-1]) && + moveBackward() + ) {} + // Move until we find the first non-alphanumeric + while( + column -1 >= 0 && + isCharAlphanumeric(promptText[column-1]) && + moveBackward() + ) {} + }; + + function isCharAlphanumeric(charToTest) { + if(typeof charToTest == 'string') { + var code = charToTest.charCodeAt(); + return (code >= 'A'.charCodeAt() && code <= 'Z'.charCodeAt()) || + (code >= 'a'.charCodeAt() && code <= 'z'.charCodeAt()) || + (code >= '0'.charCodeAt() && code <= '9'.charCodeAt()); + } + return false; + }; + + function doComplete() { + if(typeof config.completeHandle == 'function') { + var completions = config.completeHandle(promptText); + var len = completions.length; + if (len === 1) { + extern.promptText(promptText + completions[0]); + } else if (len > 1 && config.cols) { + var prompt = promptText; + // Compute the number of rows that will fit in the width + var max = 0; + for (var i = 0;i < len;i++) { + max = Math.max(max, completions[i].length); + } + max += 2; + var n = Math.floor(config.cols / max); + var buffer = ""; + var col = 0; + for (i = 0;i < len;i++) { + var completion = completions[i]; + buffer += completions[i]; + for (var j = completion.length;j < max;j++) { + buffer += " "; + } + if (++col >= n) { + buffer += "\n"; + col = 0; + } + } + commandResult(buffer,"jquery-console-message-value"); + extern.promptText(prompt); + } + } + }; + + function doNothing() {}; + + extern.promptText = function(text){ + if (typeof text === 'string') { + promptText = text; + column = promptText.length; + updatePromptDisplay(); + } + return promptText; + }; + + //////////////////////////////////////////////////////////////////////// + // Update the prompt display + function updatePromptDisplay(){ + var line = promptText; + var html = ''; + if (column > 0 && line == ''){ + // When we have an empty line just display a cursor. + html = cursor; + } else if (column == promptText.length){ + // We're at the end of the line, so we need to display + // the text *and* cursor. + html = htmlEncode(line) + cursor; + } else { + // Grab the current character, if there is one, and + // make it the current cursor. + var before = line.substring(0, column); + var current = line.substring(column,column+1); + if (current){ + current = + '' + + htmlEncode(current) + + ''; + } + var after = line.substring(column+1); + html = htmlEncode(before) + current + htmlEncode(after); + } + prompt.html(html); + scrollToBottom(); + }; + + // Simple HTML encoding + // Simply replace '<', '>' and '&' + // TODO: Use jQuery's .html() trick, or grab a proper, fast + // HTML encoder. + function htmlEncode(text){ + return ( + text.replace(/&/g,'&') + .replace(/') + ); + }; + + return extern; + }; + // Simple utility for printing messages + $.fn.filledText = function(txt){ + $(this).text(txt); + $(this).html($(this).html().replace(/\n/g,'
')); + return this; + }; + + // Alternative method for focus without scrolling + $.fn.focusWithoutScrolling = function(){ + var x = window.scrollX, y = window.scrollY; + $(this).focus(); + window.scrollTo(x, y); + }; +})(jQuery); diff --git a/resources/public/javascript/jquery.console.js b/resources/public/javascript/jquery.console.js deleted file mode 100644 index aae45ce..0000000 --- a/resources/public/javascript/jquery.console.js +++ /dev/null @@ -1,486 +0,0 @@ -// JQuery Console 1.0 -// Sun Feb 21 20:28:47 GMT 2010 -// -// Copyright 2010 Chris Done. All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions -// are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. - -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY CHRIS DONE ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL CHRIS DONE OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT -// OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR -// BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH -// DAMAGE. - -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed or -// implied, of Chris Done. -// -// TESTED ON -// Internet Explorer 6 -// Opera 10.01 -// Chromium 4.0.237.0 (Ubuntu build 31094) -// Firefox 3.5.8 - -(function($){ - $.fn.console = function(config){ - //////////////////////////////////////////////////////////////////////// - // Constants - // Some are enums, data types, others just for optimisation - var keyCodes = { left:37,right:39,up:38,down:40,back:8,del:46, - end:35,start:36,ret:13 }; - var cursor = ' '; - // Opera only works with this character, not or ­, - // but IE6 displays this character, which is bad, so just use - // it on Opera. - var wbr = $.browser.opera? '​' : ''; - - //////////////////////////////////////////////////////////////////////// - // Globals - var container = $(this); - var inner = $('
'); - var typer = $(''); - // Prompt - var promptBox; - var prompt; - var promptLabel = config && config.promptLabel? config.promptLabel : "> "; - var column = 0; - var promptText = ''; - var restoreText = ''; - // Prompt history stack - var history = []; - var ringn = 0; - // For reasons unknown to The Sword of Michael himself, Opera - // triggers and sends a key character when you hit various - // keys like PgUp, End, etc. So there is no way of knowing - // when a user has typed '#' or End. My solution is in the - // typer.keydown and typer.keypress functions; I use the - // variable below to ignore the keypress event if the keydown - // event succeeds. - var cancelKeyPress = 0; - - // External exports object - var extern = {}; - - //////////////////////////////////////////////////////////////////////// - // Main entry point - (function(){ - container.append(inner); - inner.append(typer); - typer.css({position:'absolute',top:0,left:'-9999px'}); - if (config.welcomeMessage) - message(config.welcomeMessage,'jquery-console-welcome'); - newPromptBox(); - if (config.autofocus) { - inner.addClass('jquery-console-focus'); - typer.focus(); - setTimeout(function(){ - inner.addClass('jquery-console-focus'); - typer.focus(); - },100); - } - extern.inner = inner; - extern.scrollToBottom = scrollToBottom; - })(); - - //////////////////////////////////////////////////////////////////////// - // Reset terminal - extern.reset = function(){ - var welcome = true; - inner.parent().fadeOut(function(){ - inner.find('div').each(function(){ - if (!welcome) - $(this).remove(); - welcome = false; - }); - newPromptBox(); - inner.parent().fadeIn(function(){ - inner.addClass('jquery-console-focus'); - typer.focus(); - }); - }); - }; - - //////////////////////////////////////////////////////////////////////// - // Reset terminal - extern.notice = function(msg,style){ - var n = $('
').append($('
').text(msg)) - .css({visibility:'hidden'}); - container.append(n); - var focused = true; - if (style=='fadeout') - setTimeout(function(){ - n.fadeOut(function(){ - n.remove(); - }); - },4000); - else if (style=='prompt') { - var a = $('
OK
'); - n.append(a); - focused = false; - a.click(function(){ n.fadeOut(function(){ n.remove();inner.css({opacity:1}) }); }); - } - var h = n.height(); - n.css({height:'0px',visibility:'visible'}) - .animate({height:h+'px'},function(){ - if (!focused) inner.css({opacity:0.5}); - }); - n.css('cursor','default'); - return n; - }; - - //////////////////////////////////////////////////////////////////////// - // Make a new prompt box - function newPromptBox() { - column = 0; - promptText = ''; - promptBox = $('
'); - var label = $(''); - promptBox.append(label.text(promptLabel).show()); - prompt = $(''); - promptBox.append(prompt); - inner.append(promptBox); - updatePromptDisplay(); - }; - - //////////////////////////////////////////////////////////////////////// - // Handle setting focus - container.click(function(){ - inner.addClass('jquery-console-focus'); - inner.removeClass('jquery-console-nofocus'); - typer.focus(); - scrollToBottom(); - return false; - }); - - //////////////////////////////////////////////////////////////////////// - // Handle losing focus - typer.blur(function(){ - inner.removeClass('jquery-console-focus'); - inner.addClass('jquery-console-nofocus'); - }); - - //////////////////////////////////////////////////////////////////////// - // Handle key hit before translation - // For picking up control characters like up/left/down/right - - typer.keydown(function(e){ - cancelKeyPress = 0; - var keyCode = e.keyCode; - if (isControlCharacter(keyCode)) { - cancelKeyPress = keyCode; - if (!typer.consoleControl(keyCode)) { - return false; - } - } - }); - - //////////////////////////////////////////////////////////////////////// - // Handle key press - typer.keypress(function(e){ - var keyCode = e.keyCode || e.which; - if (cancelKeyPress != keyCode && keyCode >= 32){ - if (cancelKeyPress) return false; - if (typeof config.charInsertTrigger == 'undefined' || - (typeof config.charInsertTrigger == 'function' && - config.charInsertTrigger(keyCode,promptText))) - typer.consoleInsert(keyCode); - } - if ($.browser.webkit) return false; - }); - - // Is a keycode a contorl character? - // E.g. up, down, left, right, backspc, return, etc. - function isControlCharacter(keyCode){ - // TODO: Make more precise/fast. - return ( - (keyCode >= keyCodes.left && keyCode <= keyCodes.down) - || keyCode == keyCodes.back || keyCode == keyCodes.del - || keyCode == keyCodes.end || keyCode == keyCodes.start - || keyCode == keyCodes.ret - ); - }; - - //////////////////////////////////////////////////////////////////////// - // Handle console control keys - // E.g. up, down, left, right, backspc, return, etc. - typer.consoleControl = function(keyCode){ - switch (keyCode){ - case keyCodes.left:{ - moveColumn(-1); - updatePromptDisplay(); - return false; - break; - } - case keyCodes.right:{ - moveColumn(1); - updatePromptDisplay(); - return false; - break; - } - case keyCodes.back:{ - if (moveColumn(-1)){ - deleteCharAtPos(); - updatePromptDisplay(); - } - return false; - break; - } - case keyCodes.del:{ - if (deleteCharAtPos()) - updatePromptDisplay(); - return false; - break; - } - case keyCodes.end:{ - if (moveColumn(promptText.length-column)) - updatePromptDisplay(); - return false; - break; - } - case keyCodes.start:{ - if (moveColumn(-column)) - updatePromptDisplay(); - return false; - break; - } - case keyCodes.ret:{ - commandTrigger(); return false; - } - case keyCodes.up:{ - rotateHistory(-1); return false; - } - case keyCodes.down:{ - rotateHistory(1); return false; - } - default: //alert("Unknown control character: " + keyCode); - } - }; - - //////////////////////////////////////////////////////////////////////// - // Rotate through the command history - function rotateHistory(n){ - if (history.length == 0) return; - ringn += n; - if (ringn < 0) ringn = history.length; - else if (ringn > history.length) ringn = 0; - var prevText = promptText; - if (ringn == 0) { - promptText = restoreText; - } else { - promptText = history[ringn - 1]; - } - if (config.historyPreserveColumn) { - if (promptText.length < column + 1) { - column = promptText.length; - } else if (column == 0) { - column = promptText.length; - } - } else if (config.historyColumnAtEnd) { - column = promptText.length; - } else { - column = 0; - } - updatePromptDisplay(); - }; - - // Add something to the history ring - function addToHistory(line){ - history.push(line); - restoreText = ''; - }; - - // Delete the character at the current position - function deleteCharAtPos(){ - if (promptText != ''){ - promptText = - promptText.substring(0,column) + - promptText.substring(column+1); - restoreText = promptText; - return true; - } else return false; - }; - - //////////////////////////////////////////////////////////////////////// - // Validate command and trigger it if valid, or show a validation error - function commandTrigger() { - var line = promptText; - if (typeof config.commandValidate == 'function') { - var ret = config.commandValidate(line); - if (ret == true || ret == false) { - if (ret) { - handleCommand(); - } - } else { - commandResult(ret,"jquery-console-message-error"); - } - } else { - handleCommand(); - } - }; - - // Scroll to the bottom of the view - function scrollToBottom() { - inner.attr({ scrollTop: inner.attr("scrollHeight") });; - }; - - //////////////////////////////////////////////////////////////////////// - // Handle a command - function handleCommand() { - if (typeof config.commandHandle == 'function') { - var ret = config.commandHandle(promptText,function(msgs){ - commandResult(msgs); - }); - if (typeof ret == 'boolean') { - if (ret) { - // Command succeeded without a result. - addToHistory(promptText); - commandResult(); - } else { - addToHistory(promptText); - commandResult('Command failed.', - "jquery-console-message-error"); - } - } else if (typeof ret == "string") { - addToHistory(promptText); - commandResult(ret,"jquery-console-message-success"); - } else if (typeof ret == 'undefined') { - addToHistory(promptText); - } else if (ret.length) { - addToHistory(promptText); - commandResult(ret); - } - } - }; - - //////////////////////////////////////////////////////////////////////// - // Reset the prompt in invalid command - function commandResult(msg,className) { - column = -1; - updatePromptDisplay(); - if (typeof msg == 'string') { - message(msg,className); - } else { - for (var x in msg) { - var ret = msg[x]; - message(ret.msg,ret.className); - } - } - newPromptBox(); - }; - - //////////////////////////////////////////////////////////////////////// - // Display a message - function message(msg,className) { - var mesg = $('
'); - if (className) mesg.addClass(className); - mesg.html(msg).hide(); - inner.append(mesg); - mesg.show(); - }; - - //////////////////////////////////////////////////////////////////////// - // Handle normal character insertion - typer.consoleInsert = function(keyCode){ - // TODO: remove redundant indirection - var char = String.fromCharCode(keyCode); - var before = promptText.substring(0,column); - var after = promptText.substring(column); - promptText = before + char + after; - moveColumn(1); - restoreText = promptText; - updatePromptDisplay(); - }; - - //////////////////////////////////////////////////////////////////////// - // Move to another column relative to this one - // Negative means go back, positive means go forward. - function moveColumn(n){ - if (column + n >= 0 && column + n <= promptText.length){ - column += n; - return true; - } else return false; - }; - - extern.promptText = function(text){ - if (text) { - promptText = text; - if (column > promptText.length) - column = promptText.length; - updatePromptDisplay(); - } - return promptText; - }; - - - //////////////////////////////////////////////////////////////////////// - // Update the prompt display - function updatePromptDisplay(){ - var line = promptText; - var html = ''; - if (column > 0 && line == ''){ - // When we have an empty line just display a cursor. - html = cursor; - } else if (column == promptText.length){ - // We're at the end of the line, so we need to display - // the text *and* cursor. - html = htmlEncode(line) + cursor; - } else { - // Grab the current character, if there is one, and - // make it the current cursor. - var before = line.substring(0, column); - var current = line.substring(column,column+1); - if (current){ - current = - '' + - htmlEncode(current) + - ''; - } - var after = line.substring(column+1); - html = htmlEncode(before) + current + htmlEncode(after); - } - prompt.html(html); - scrollToBottom(); - }; - - // Simple HTML encoding - // Simply replace '<', '>' and '&' - // TODO: Use jQuery's .html() trick, or grab a proper, fast - // HTML encoder. - function htmlEncode(text){ - return ( - text.replace(/&/g,'&') - .replace(/&]{10})/g,'$1­' + wbr) - ); - }; - - return extern; - }; - // Simple utility for printing messages - $.fn.filledText = function(txt){ - $(this).text(txt); - $(this).html($(this).html().replace(/\n/g,'
')); - return this; - }; -})(jQuery); diff --git a/resources/public/javascript/tryclojure.js b/resources/public/javascript/tryclojure.js index 624f046..c6069fc 100644 --- a/resources/public/javascript/tryclojure.js +++ b/resources/public/javascript/tryclojure.js @@ -1,10 +1,80 @@ +var currentPage = -1; +var pages = [ + "page1", + "page2", + "page3", + "page4", + "page5", + "page6", + "page7", + "page8", + "page9", + "page10", + "page11", + "end" + ]; +var pageExitConditions = [ + { + verify: function(data) { return false; } + }, + { + verify: function(data) { return data.expr == "(+ 3 3)"; } + }, + { + verify: function(data) { return data.expr == "(/ 10 3)"; } + }, + { + verify: function(data) { return data.expr == "(/ 10 3.0)"; } + }, + { + verify: function(data) { return data.expr == "(+ 1 2 3 4 5 6)"; } + }, + { + verify: function (data) { return data.expr == "(defn square [x] (* x x))"; } + }, + { + verify: function (data) { return data.expr == "(square 10)"; } + }, + { + verify: function (data) { return data.expr == "((fn [x] (* x x)) 10)"; } + }, + { + verify: function (data) { return data.expr == "(def square (fn [x] (* x x)))"; } + }, + { + verify: function (data) { return data.expr == "(map inc [1 2 3 4])"; } + }, + { + verify: function (data) { return false; } + }, + { + verify: function (data) { return false; } + } +]; + +function goToPage(pageNumber) { + if (pageNumber == currentPage || pageNumber < 0 || pageNumber >= pages.length) { + return; + } + + currentPage = pageNumber; + + var block = $("#changer"); + block.fadeOut(function(e) { + block.load("/tutorial", { 'page' : pages[pageNumber] }, function() { + block.fadeIn(); + changerUpdated(); + }); + }); +} + function setupLink(url) { return function(e) { $("#changer").load(url, function(data) { $("#changer").html(data); }); } } function setupExamples(controller) { $(".code").click(function(e) { - controller.promptText($(this).text()); + controller.promptText($(this).text()); }); } @@ -12,45 +82,113 @@ function getStep(n, controller) { $("#tuttext").load("tutorial", { step: n }, function() { setupExamples(controller); }); } -$(document).ready( - function() { - var controller = $("#console").console({ - promptLabel: 'Clojure> ', - commandValidate:function(line){ - if (line == "") return false; - else return true; - }, - commandHandle:function(line){ - var data; - jQuery.ajax({ - url: "magics" + "?code=" + encodeURIComponent(line), - success: function(result) {data = result;}, - async: false - }); - return [{msg: data, - className:"jquery-console-message-value"}]; - }, - welcomeMessage:'Enter some Clojure code, and it will be evaluated.', - autofocus:true, - animateScroll:true, - promptHistory:true - }); +function eval_clojure(code) { + var data; + $.ajax({ + url: "eval.json", + data: { expr : code }, + async: false, + success: function(res) { data = res; } + }); + return data; +} - $("#about").click(setupLink("about")); - $("#links").click(setupLink("links")); - $("#tutorial").click(function(e) { - $("#changer").load("tutorial", {step: 0}, function(data) { - var step = 1; - $("#continue").click(function(e) { - if(step < 6 ) { step += 1; } - getStep(step, controller); - $("#tuttext").scrollTop(0); - }); - $("#back").click(function(e) { - if(step > 1) { step -= 1; } - getStep(step, controller); - $("#tuttext").scrollTop(0); - }); - }); - }); - }); \ No newline at end of file +function html_escape(val) { + var result = val; + result = result.replace(/\n/g, "
"); + result = result.replace(/[<]/g, "<"); + result = result.replace(/[>]/g, ">"); + return result; +} + +function doCommand(input) { + if (input.match(/^gopage /)) { + goToPage(parseInt(input.substring("gopage ".length))); + return true; + } + + switch (input) { + case 'next': + case 'forward': + goToPage(currentPage + 1); + return true; + case 'previous': + case 'prev': + case 'back': + goToPage(currentPage - 1); + return true; + case 'restart': + case 'reset': + case 'home': + case 'quit': + goToPage(0); + return true; + default: + return false; + } +} + +function onValidate(input) { + return (input != ""); +} + +function onHandle(line, report) { + var input = $.trim(line); + + // handle commands + if (doCommand(input)) { + report(); + return; + } + + // perform evaluation + var data = eval_clojure(input); + + // handle error + if (data.error) { + return [{msg: data.message, className: "jquery-console-message-error"}]; + } + + // handle page + if (currentPage >= 0 && pageExitConditions[currentPage].verify(data)) { + goToPage(currentPage + 1); + } + + // display expr results + return [{msg: data.result, className: "jquery-console-message-value"}]; +} + +/** + * This should be called anytime the changer div is updated so it can rebind event listeners. + * Currently this is just to make the code elements clickable. + */ +function changerUpdated() { + $("#changer code.expr").each(function() { + $(this).css("cursor", "pointer"); + $(this).attr("title", "Click to insert '" + $(this).text() + "' into the console."); + $(this).click(function(e) { + controller.promptText($(this).text()); + controller.inner.click(); + }); + }); +} + +var controller; + +$(document).ready(function() { + controller = $("#console").console({ + welcomeMessage:'Give me some Clojure:', + promptLabel: '> ', + commandValidate: onValidate, + commandHandle: onHandle, + autofocus:true, + animateScroll:true, + promptHistory:true + }); + + $("#about").click(setupLink("about")); + $("#links").click(setupLink("links")); + $("#home").click(setupLink("home")); + + changerUpdated(); +}); diff --git a/resources/public/tutorial/end.html b/resources/public/tutorial/end.html new file mode 100644 index 0000000..f85f040 --- /dev/null +++ b/resources/public/tutorial/end.html @@ -0,0 +1,7 @@ +

+ Glad to know you're eager for more, but unfortunately this tutorial is over. +

+ +

+ Maybe you can click the 'links' button above for more resources? +

diff --git a/resources/public/tutorial/page1.html b/resources/public/tutorial/page1.html new file mode 100644 index 0000000..4654fcb --- /dev/null +++ b/resources/public/tutorial/page1.html @@ -0,0 +1,7 @@ +

+ I'll take you on a 5-minutes tour of Clojure, but feel free to experiment on your own along the road! +

+ +

+ You can type next to skip forward, back to return to the previous step, and restart to get back to the beginning. Let's get started: type next. +

diff --git a/resources/public/tutorial/page10.html b/resources/public/tutorial/page10.html new file mode 100644 index 0000000..f1e2228 --- /dev/null +++ b/resources/public/tutorial/page10.html @@ -0,0 +1,35 @@ +

+ Success! Now you can call this new square function just like you called the old square function. +

+ +

+ By now, you know that lists are quite important in Clojure. + But Clojure also has other data structures: +

+ +

+ Vectors: [1 2 3 4]
+ Maps: {:foo "bar" 3 4}
+ Sets: #{1 2 3 4}
+

+ +

+ Vectors and lists are sequential and ordered collections. + Sets are not ordered, and they cannot contain duplicate elements. + Maps are key-value collections, where the keys can be any object. + Here, we've used what Clojure calls a keyword (:foo) for one of the keys, and a number for the other key. +

+ +

+ Now I'll tell you another thing that may surprise you: Clojure collections are immutable - they can never change. + When you do anything on a list, including adding and removing elements, you actually get a brand new list. + (Fortunately, Clojure is amazingly efficient at creating new lists). + In general, Clojure encourages you to have as little mutable state as possible. + For example, instead of "for" loops and other state-changing constructs, most of the time you'll see functions doing transformations on immutable data and returning new collections, without changing the old one. +

+ +

+ A prime example of this is map. map is a higher order function, which means that it takes another function as an argument. + For example, you can ask map to increment each number in a vector by passing it the inc function, followed by the vector. + Try it for yourself: type (map inc [1 2 3 4]) to continue. +

diff --git a/resources/public/tutorial/page11.html b/resources/public/tutorial/page11.html new file mode 100644 index 0000000..726fe3c --- /dev/null +++ b/resources/public/tutorial/page11.html @@ -0,0 +1,11 @@ +

Great job!

+ +

+ We've only scratched the surface of Clojure and its mind-bending power. + This tutorial is still a work in progress, and I'm working on more steps. + Meanwhile, you can learn more about Clojure by visiting the 'links' page. +

+ +

+ Welcome to your adventures in Clojure, and be prepared to be surprised and delighted every step of the way! +

diff --git a/resources/public/tutorial/page2.html b/resources/public/tutorial/page2.html new file mode 100644 index 0000000..1d11c62 --- /dev/null +++ b/resources/public/tutorial/page2.html @@ -0,0 +1,7 @@ +

+ The first thing you may notice about Clojure is that common operations look... strange. +

+ +

+ For example, try typing (+ 3 3) in the REPL. +

diff --git a/resources/public/tutorial/page3.html b/resources/public/tutorial/page3.html new file mode 100644 index 0000000..ae8cc9b --- /dev/null +++ b/resources/public/tutorial/page3.html @@ -0,0 +1,13 @@ +

+ That was a strange way to say "three plus three", wasn't it? +

+ +

+ A Clojure program is made of lists. + (+ 3 3) is a list that contains an operator, and then the operands. + Try out the same concept with the * and - operators. +

+ +

+ Division might surprise you. When you're ready to move forward, try (/ 10 3). +

diff --git a/resources/public/tutorial/page4.html b/resources/public/tutorial/page4.html new file mode 100644 index 0000000..991dc20 --- /dev/null +++ b/resources/public/tutorial/page4.html @@ -0,0 +1,4 @@ +

+ Now, that was a bit surprising: Clojure has a built in Rational type. + You can still force Clojure to do floating point division by making one of the operands floating point: type (/ 10 3.0) to continue. +

diff --git a/resources/public/tutorial/page5.html b/resources/public/tutorial/page5.html new file mode 100644 index 0000000..1788c09 --- /dev/null +++ b/resources/public/tutorial/page5.html @@ -0,0 +1,6 @@ +

Awesome!

+ +

+ Many Clojure functions can take an arbitrary number of arguments. + Try it out: type (+ 1 2 3 4 5 6) to continue. +

diff --git a/resources/public/tutorial/page6.html b/resources/public/tutorial/page6.html new file mode 100644 index 0000000..daa7698 --- /dev/null +++ b/resources/public/tutorial/page6.html @@ -0,0 +1,8 @@ +

+ That's enough math. Let's do some fun stuff, like defining functions. + You can do that in Clojure with defn. +

+ +

+ Type (defn square [x] (* x x)) to define a "square" function that takes a single number and squares it. +

diff --git a/resources/public/tutorial/page7.html b/resources/public/tutorial/page7.html new file mode 100644 index 0000000..e6c58c0 --- /dev/null +++ b/resources/public/tutorial/page7.html @@ -0,0 +1,13 @@ +

Congratulations - you just defined your first Clojure function. Many more will follow!

+ +

+ defn takes the name of the function, then the list of arguments, and then the body of the function. + I told you that a Clojure program is made of lists, right? + The entire defn is a list, and the function body is also a list. + (Even the arguments are collected in a vector, which is similar to a list - we'll talk about vectors soon). +

+ +

+ Oh, sorry for talking so long - you probably want to try out your brand new function! + Type (square 10). +

diff --git a/resources/public/tutorial/page8.html b/resources/public/tutorial/page8.html new file mode 100644 index 0000000..d3a26cf --- /dev/null +++ b/resources/public/tutorial/page8.html @@ -0,0 +1,20 @@ +

Yay! It works!

+ +

+ By now, you probably think that Clojure is very different from the programming languages you already know. + Indeed, it belongs to a different family than most popular languages' - the family of "functional" programming languages. + Like most functional languages, Clojure can define a function without even giving it a name: +

+ +(fn [x] (* x x)) + +

+ If you run this code, you'll see some cryptic output. + In Clojure, functions are just normal values like numbers or strings. + fn defines a function and then returns it. + What you're seeing is simply what a function looks like when you print it on the screen. +

+ +

+ But wait - an anonymous function isn't very useful if you can't call it. Try to define a new anonymous function and call it straight away: ((fn [x] (* x x)) 10). +

diff --git a/resources/public/tutorial/page9.html b/resources/public/tutorial/page9.html new file mode 100644 index 0000000..44e17cc --- /dev/null +++ b/resources/public/tutorial/page9.html @@ -0,0 +1,16 @@ +

+ Let's see what you just did: you evaluated a list where the first element is the function itself, defined on the spot - and the other elements are the arguments that you pass to the function. + That's exactly the same syntax that you used earlier on to call functions like square or even +. + The only difference is that now you defined the function in the same place where you called it. +

+ +

+ Remember defn? + Now I can tell you a secret: defn is actually just a bit of syntactic sugar around def and fn. + You've just seen fn at work: it defines a new function. + def binds the newly defined function to a name. +

+ +

+ If you want, you can create a named function without using defn: type (def square (fn [x] (* x x))) to continue. +

diff --git a/src/tryclojure/core.clj b/src/tryclojure/core.clj deleted file mode 100644 index f6eb808..0000000 --- a/src/tryclojure/core.clj +++ /dev/null @@ -1,189 +0,0 @@ -(ns tryclojure.core - (:use ring.adapter.jetty - net.licenser.sandbox - [net.licenser.sandbox tester matcher] - [hiccup core form-helpers page-helpers] - [ring.middleware reload stacktrace params file session] - net.cgrand.moustache - [clojure.stacktrace :only [root-cause]] - tryclojure.tutorial) - (:import java.io.StringWriter - java.util.concurrent.TimeoutException)) - -(enable-security-manager) - -(def sandbox-tester - (extend-tester secure-tester - (whitelist - (function-matcher 'println 'print 'pr 'prn 'var 'print-doc 'doc 'throw - 'def 'def* 'dosync 'alter '. 'finally '*math-context*) - (class-matcher java.io.StringWriter String Byte Character StrictMath StringBuffer - java.net.URL java.net.URI java.math.MathContext)))) - -(def my-obj-tester - (extend-tester default-obj-tester - (whitelist - (class-matcher java.io.StringWriter String Byte Character StrictMath StringBuffer - java.net.URL java.net.URI java.math.MathContext)))) - -(def state-tester - (new-tester (whitelist (constantly '(true))) - (blacklist (function-matcher 'def 'def* - 'ensure 'ref-set 'alter 'commute - 'swap! 'compare-and-set!)))) - -(defn has-state? [form] - (not (state-tester form nil))) - -(defn execute-text [txt history] - (let [sc (new-sandbox-compiler :tester sandbox-tester - :timeout 1000) - result (try - (loop [history history] - (when (not (empty? history)) - (do - ((sc (first history))) - (recur (next history))))) - (let [form (binding [*read-eval* false] (read-string txt))] - (with-open [writer (java.io.StringWriter.)] - (let [r (pr-str ((sc form) {'*out* writer}))] - [(str (.replace (escape-html writer) "\n" "
") (code (str r))) - (if (has-state? form) (conj history form) history)]))) - (catch OutOfMemoryError _ ["Out of memory error was thrown. Cleaning up all defs." nil]) - (catch TimeoutException _ ["Execution Timed Out!" history]) - (catch SecurityException e - [(if (.startsWith - (.getMessage e) - "Code did not pass sandbox guidelines: ") - (str e - "

This error was caused because you tried to use a function that " - " isn't whitelisted in the sandbox. The sandbox's whitelist is probably missing some" - " useful functions that should be whitelisted. If you think the function you tried to " - "use is safe and should be whitelisted, please file an issue at " - "http://github.com/Raynes/tryclojure/issues or mention it to Raynes on the " - "#clojure or #clojure-casual IRC channels on the FreeNode network.") - (str (root-cause e))) - history]) - (catch Exception e [(str (root-cause e)) history]))] - result)) - -(def links - (html (unordered-list - [(link-to "http://clojure.org" "The official Clojure website") - (link-to "http://www.assembla.com/wiki/show/clojure/Getting_Started" "Getting started with Clojure") - (link-to "http://groups.google.com/group/clojure" "Clojure mailing list") - (link-to "http://java.ociweb.com/mark/clojure/article.html" "A comprehensive Clojure tutorial") - (link-to "http://joyofclojure.com/" "The Joy of Clojure: a book by Michael Fogus and Chris Houser") - (link-to "http://www.pragprog.com/titles/shcloj/programming-clojure" "Programming Clojure, a book by Stuart Halloway") - (link-to "http://disclojure.org" "Disclojure") - (link-to "http://planet.clojure.in" "Planet Clojure")]))) - -(def bottom-html - (html [:p.bottom - "This site is still under construction. I can't promise everything will work correctly." - " You can find the site's source and such on it's " (link-to "http://github.com/Raynes/tryclojure" "github") - " page."] - [:p.bottom - "This website is mostly going to be useful for beginners. All of the code that is ran is ran" - " server side. This means it *has* to be sandboxed. This also means that this web REPL is NOT" - " ever going to be as useful as a normal REPL ran on your computer. I'm considering throwing up" - " another site that runs a pretty Clojure REPL through a typical Java applet. That one would be" - " client-side, and would not be sandboxed."] - [:p.bottom - "TryClojure is written in Clojure and JavaScript, powered by " - (link-to "http://github.com/Licenser/clj-sandbox" "clj-sandbox") - " and Chris Done's " - (link-to "http://github.com/chrisdone/jquery-console" "jquery-console")] - [:p.bottom - "Huge thanks to " (link-to "http://www.bestinclass.dk/" "Lau Jensen") - " for lots of help with everything ranging from Gimp, to straight up CSS and HTML design tips."] - [:p.bottom - "Also thanks to my buddy Heinz (Licenser) for all the help with this site. This site is hosted on" - " his server, with his domain name. He completely funds this project and it wouldn't be possible without" - " him. Not to mention his wonderful clj-sandbox and clj-highlight libraries make this site possible!"])) - -(def fire-html - (html - (:html4 doctype) - [:head - (include-css "/resources/public/css/tryclojure.css") - (include-js "/resources/public/javascript/jquery-1.4.2.min.js" - "/resources/public/javascript/jquery.console.js" - "/resources/public/javascript/tryclojure.js") - [:title "TryClojure"]] - [:body - [:script {:type "text/javascript"} - " - var _gaq = _gaq || []; - _gaq.push(['_setAccount', 'UA-552543-3']); - _gaq.push(['_trackPageview']); - - (function() { - var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; - ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; - var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); - })(); -"] - [:div {:style "text-align: center;"} [:h1 "Try Clojure"]] - [:div#container [:div#console.console] - [:table.bottom {:border "0"} - [:tr] - [:td.bholder [:div.buttons - [:a#tutorial.buttons "tutorial"] - [:a#links.buttons "links"] - [:a#about.lbutton "about"]]] - [:tr] - [:td [:div#changer [:p.bottom - "Welcome to TryClojure. Above, you have a Clojure REPL. You can type expressions and see " - "their results right here in your browser. We also have a brief tutorial to give you a " - "taste of Clojure. Try it out!"]]]]] - [:div.footer - [:p.bottom "©2010 Anthony Simpson (Raynes)"] - [:p.bottom "Domain and hosting kindly provided by " - (link-to "http://blog.licenser.net" "Heinz N. Gies") "."]]])) - -(defn handler [{session :session}] - {:status 200 - :headers {"Content-Type" "text/html"} - :session session - :body fire-html}) - -(defn div-handler [{qparams :query-params session :session}] - (let [[result history] (execute-text (qparams "code") (or (:history session) []))] - {:status 200 - :headers {"Content-Type" "text/txt"} - :session {:history history} - :body result})) - -(defn about-handler [{session :session}] - {:status 200 - :headers {"Content-Type" "text/html"} - :session session - :body bottom-html}) - -(defn link-handler [{session :session}] - {:status 200 - :headers {"Content-Type" "text/html"} - :session session - :body links}) - -(defn tutorial-handler [{formps :form-params session :session :as req}] - {:status 200 - :headers {"Content-Type" "text/html"} - :session session - :body (get-tutorial (formps "step"))}) - -(def clojureroutes - (app - (wrap-session) - ;(wrap-reload '(tryclojure.core tryclojure.tutorial)) - (wrap-file (System/getProperty "user.dir")) - (wrap-params) - (wrap-stacktrace) - ["tutorial"] tutorial-handler - ["links"] link-handler - ["about"] about-handler - ["magics"] div-handler - [""] handler)) - -(defn tryclj [] (run-jetty #'clojureroutes {:port 8801})) diff --git a/src/tryclojure/models/eval.clj b/src/tryclojure/models/eval.clj new file mode 100644 index 0000000..810f862 --- /dev/null +++ b/src/tryclojure/models/eval.clj @@ -0,0 +1,40 @@ +(ns tryclojure.models.eval + (:require [clojail.testers :refer [secure-tester-without-def blanket]] + [clojail.core :refer [sandbox]] + [clojure.stacktrace :refer [root-cause]] + [noir.session :as session]) + (:import java.io.StringWriter + java.util.concurrent.TimeoutException)) + +(defn eval-form [form sbox] + (with-open [out (StringWriter.)] + (let [result (sbox form {#'*out* out})] + {:expr form + :result [out result]}))) + +(defn eval-string [expr sbox] + (let [form (binding [*read-eval* false] (read-string expr))] + (eval-form form sbox))) + +(def try-clojure-tester + (conj secure-tester-without-def (blanket "tryclojure" "noir"))) + +(defn make-sandbox [] + (sandbox try-clojure-tester + :timeout 2000 + :init '(do (require '[clojure.repl :refer [doc source]]) + (future (Thread/sleep 600000) + (-> *ns* .getName remove-ns))))) + +(defn find-sb [old] + (if-let [sb (get old "sb")] + old + (assoc old "sb" (make-sandbox)))) + +(defn eval-request [expr] + (try + (eval-string expr (get (session/swap! find-sb) "sb")) + (catch TimeoutException _ + {:error true :message "Execution Timed Out!"}) + (catch Exception e + {:error true :message (str (root-cause e))}))) diff --git a/src/tryclojure/server.clj b/src/tryclojure/server.clj new file mode 100644 index 0000000..010ce2f --- /dev/null +++ b/src/tryclojure/server.clj @@ -0,0 +1,23 @@ +(ns tryclojure.server + (:use compojure.core) + (:require [compojure.route :as route] + [noir.util.middleware :as nm] + [ring.adapter.jetty :as jetty] + [tryclojure.views.home :as home] + [tryclojure.views.tutorial :as tutorial] + [tryclojure.views.eval :as eval])) + +(def app-routes + [(GET "/" [] (home/root-html)) + (GET "/about" [] (home/about-html)) + (GET "/links" [] (home/links-html)) + (POST "/tutorial" [:as {args :params}] (tutorial/tutorial-html (args :page))) + (POST "/eval.json" [:as {args :params}] (eval/eval-json (args :expr) (args :jsonp))) + (GET "/eval.json" [:as {args :params}] (eval/eval-json (args :expr) (args :jsonp))) + (route/resources "/") + (route/not-found "Not Found")]) + +(def app (nm/app-handler app-routes)) + +(defn -main [port] + (jetty/run-jetty app {:port (Long. port) :join? false})) diff --git a/src/tryclojure/tutorial.clj b/src/tryclojure/tutorial.clj deleted file mode 100644 index 79f5839..0000000 --- a/src/tryclojure/tutorial.clj +++ /dev/null @@ -1,214 +0,0 @@ -(ns tryclojure.tutorial - (:use hiccup.core - clj-highlight.core - clj-highlight.syntax.clojure - clj-highlight.output.hiccup)) - -(def clj-highlighter (highlighter clj-syntax (to-hiccup) html-escape-mangler newline-to-br-mangler)) - -(defn code [code] - (html (clj-highlighter code))) - -(def tutorial0-text - (html - [:p.bottom - "This tutorial is intended for people who have never used Clojure before but have experience with " - " other programming languages. This is not meant to be a comprehensive tutorial" - ", but instead intended to get you started with Clojure -- and hopefully interested enough to continue. " - "Please bear in mind that this tutorial is evolving, so check back frequently for new additions." [:br] [:br] - "Above, you have your REPL. Please try the examples as we go along, and by all means experiment" - " with concepts indpendently. Should you refuse, you will promptly be IP banned from this website, and a teddy " - "bear will eat your candies. ;-)"[:br] [:br] "Don't make teddy eat your candy."] - [:p.bottom - "All of the examples (pretty colored text) are clickable. If you click them, they will be copied" - " into the REPL prompt above. This is for convenience, so you don't have to type tedious things like a " - "backwards string (you'll see). Copy and pasting is not allowed in the REPL, because I and the creator of " - "JQuery-console, which powers the REPL above, believe that copying and pasting isn't so great for learning." - " While these examples are clickable, I urge you to type out the examples by hand. If you type them out, " - "they will sink in better. Enjoy the tutorial."] - [:p.bottom "Hit the pretty little 'Next' button to continue for the love of Pete!"])) - -(def tutorial1-text - (html - [:p.bottom - "Let's jump right in there and write some Clojure. Let's start out with the familiar 'Hello, World!' People " - "seem to love it so much. Type the following into the REPL: "] - (code "(println \"!dlrow ,olleH\")") - [:p.bottom - "Something isn't right here! (I'm not talking about the fact that this website's lack of panache) " - "It seems that 'Hello World' has been mangled! It's okay though, because we're smart " - "and we can figure out how to remedy this situation. If you're wondering why it printed our gibberish AND 'nil' " - "it's because 'nil' (which essentially means 'nothing') is println's return value." - "Let's experiment until we figure out what went wrong. " - "It seems that the " (code "reverse") " should fix things -- so type the following into the REPL: "] - (code "(reverse \"!dlrow ,olleH\")") - [:p.bottom - "Hmm. It did indeed set it right, but this isn't what we were looking for! That doesn't even look like a string. " - "Reverse took our string, reversed it, and now we have a sequence of characters. You can tell this because each " - "character has a '\\' (backslash in layman's terms) preceeding it. Clojure uses those to denote Character (big 'C' character) literals. " - "We need a string though! " - "What do we do when we need a string, but we have something else that isn't stringy (a highly technical term)? No, we don't go to the shop " - "and buy string cheese (not to disparage string cheese mind you), but instead we use the lovely " (code "str") " function! Try this: "] - (code "(str (reverse \"!dlrow ,olleH\"))") - [:p.bottom - "Ooh! So close. It appears that the str function is making the entire sequence a string! Sequences can be made to " - " look like strings. str works on any number of arguments. You can pass str any number of arguments and it will " - "call Java's toString method on each of them to change them into a string, and then it will concatenate them " - "all into one mammoth string. Unfortunately, all of our characters are stuck in this sequence! How can we get them out " - "of the sequence, and pass them all to str? Do not fear, young Clojurian, for I have the answer. We use the " - (code "apply") " function. " (code "apply") " takes a function and a sequence, and 'unrolls' the sequence, " - "passing all of its elements to the function as arguments. Basically, it takes the sequence " - "and dumps its elements into the argument list of the supplied function; which is exactly what we need. " - "Let's see if it works... try this: "] - (code "(apply str (reverse \"!dlrow ,olleH\"))") - [:p.bottom - "Ka-chow! There we go. Impressive. I'm proud of you, I really am. Now, for the final test. Let's tell the world " - "just how much they mean to us by making the world rotate in the other direction! Let's do this: "] - (code "(println (apply str (reverse \"!dlrow ,olleH\")))") - [:p.bottom - "Congratulations, you have passed step one. Press Next to continue."])) - -(def tutorial2-text - (html - [:p.bottom - "Alright. We have greeted the world. Now what? Math? Clojure is great at math. Thanks to the uniformity of " - "prefix notation, we don't have to worry about precedence rules; which we kinda loathe anyway. This goes before that, and that goes before that other thing... blah! In Clojure, mathematical operators work like any other function." - " The common operators are +, -, *, and /. Let's try them out: "] - (code "(+ 2 2)\n") - (code "(- 3 2)\n") - (code "(* 5 5)\n") - (code "(/ 4 3)\n") - [:p.bottom - "So, that was great. But that last one doesn't quite look right. The problem is that Clojure has a built in " - "Ratio type. You can confirm this by doing this: " (code "(class (/ 4 3))") "."] - [:p.bottom - "So how do we perform divison that we are familiar with? If you want decimal division, use a floating-point literal for either " - "the numerator or denominator: " (code "(/ 4.0 3)") ". If you want to stick to integer math, you can use the " (code "quot") " function, " - "or the " (code "rem") " function to get the remainder."] - [:p.bottom - "All of the math functions take an arbitrary number of arguments. This means you can do stuff like this: " - (code "(+ 1 2 3 4 5 6)") ". Go ahead, try it out. You know you want to. Play around with the math functions for " - "a while. I'll wait. Go ahead."] - [:p.bottom "That's the end of step two. Press Next to continue to step three, where we'll play with sequences."])) - -(def tutorial3-text - (html - [:p.bottom - "Now that we've covered the basics of prefix math, let's take some time to play with sequences."] - [:p.bottom - "In Clojure, the term 'sequence' is an abstraction that many data structures " - "implement. This abstraction allows any function that works on a 'seq' (pronounced seek) to work on any data " - "structure that is seqable. This means all Clojure and Java collections, strings, and even I/O streams."] - [:p.bottom - "In isolation this doesn't make sense, so let's play with vectors. A literal vector looks like this: " (code "[1 2 3 \"four\" 5 6.0]") ". Vectors are the " - "most commonly used collection in Clojure, along with maps. Let's see if we can square all of the elements " - "of a vector: " (code "(map (fn [x] (* x x)) [1 2 3 4 5])") ". Try that out. The " (code "map") " function takes " - "a function that takes a single argument and returns a value, which it 'applies' to each element of its second argument, a sequence. " - "The resulting sequence is then returned."] - [:p.bottom - "We have introduced something new here -- the anonymous function. Anonymous functions are used quite heavily in Clojure code. " - "The are created using " (code "fn") ". After fn, you supply a vector of arguments that the " - "function takes, and then the body of the function that uses those arguments to computer a value. Clojure has " - "short hand for anonymous functions as well. The above anonymous function can be rewritten using a shorthand form: " - (code "#(+ % %)") ". When you use this shorthand, the function arguments are accessed like so: % or %1 for the " - "first argument, %2 for the second argument, %3 for the third and so on. Our code now looks like this: " - (code "(map #(* % %) [1 2 3 4 5])")] - [:p.bottom - "Another highly important collection is the hashmap. A literal hashmap looks like this: " - (code "{:key \"value\" :key2 3 :key4 [3 4 2 1]}") ". The keys in this map are something called 'keywords'. " - "You can tell that they are keywords because they start with a colon ':' character. They're like symbols that " - "resolve to themselves. In idiomatic Clojure code, keywords are often used as map keys. You can use many objects as map keys, " - "including integers, vectors, strings, and other maps."] - [:p.bottom - "To get a value at a key in a map, we can do this: " (code "({:key1 :val} :key1)") ". I bet you're all lolwut " - "right now, aren't you? In Clojure, hashmaps are functions that take a single argument, which should be a key " - "and they lookup the key within themselves and return the value at the key or nil if the key doesn't exist. " - "Another neat trick for maps with keywords for keys is this: " (code "(:key1 {:key1 :val})") ". Keywords are " - "also functions. They just look themselves up in the supplied map."] - [:p.bottom - "Clojure has all sorts of other data structures as well. Sets, lists, queues, zippers, etc. We'll talk about some of " - "those later on. For now, lets move on to step four. Press Next to continue."])) - -(def tutorial4-text - (html - [:p.bottom - "We've learned a little about Clojure's sequences, so let's use them to do some stuff. I've been wondering how " - "vowels are in the word \"teddybear\". Aren't you wondering the same thing? It's absolutely agonizing not " - "knowing! We're programmers, you and I, so we shouldn't have to count those vowels ourselves. Indeed, we don't " - "We can use Clojure to count them for us!"] - [:p.bottom - "Okay, we'll start with the string \"teddybear\". We're going to be using this string a lot, and we don't want " - "to have to keep typing it over and over again, do we? Luckily, Clojure can help us here. Type this into the " - "REPL: " (code "(def teddy \"teddybear\")") ". What def does is pretty simple: it simply gives a name to a " - "value so that we can refer to it by that name later. Don't worry about what the REPL printed when you typed " - "that, it's just trying to show you exactly where the var is mapped (in this case, a sandbox generated namespace. " - "We need to make sure it worked. Type this into the REPL: " (code "teddy") ". Cool huh?"] - [:p.bottom - "Now that we are armed with a loaded teddybear, we can start figuring out how to find out the number of vowels. " - "For this, we need to introduce a new collection type: sets. A literal set looks like this: " (code "#{3 4 5 \"x\" \\y}") - ". A set can hold anything, but it can't hold any two of the same thing. In a set, there can be no duplicate objects. " - "Another important fact about sets is that they, like maps, are also functions. A set is a function that takes " - "an argument and looks inside itself to see if that same object is inside of it. If this is true, it returns the " - "object, or returns nil. Let's try this out for ourselves: "] - (code "(#{1 2 3} 3)\n") - (code "(#{\"abc\" \\e} \\e)\n") - (code "(#{3 4 \\x} 5)") - [:p.bottom - "Okay, so how is this useful? It really isn't. Not alone, anyway. However, when it's combined with other sequence " - "functions, it can be used to make a really elegant solution to a problem like this."] - [:p.bottom - "Okay, so we have a way to test if a character is a vowel. We can simply do this: " (code "(#{\\a \\e \\i \\o \\u} e)") - ". Since we're going to be using the set of vowels a lot, go ahead and give it a name in the REPL: " - (code "(def vowels #{\\a \\e \\i \\o \\u})") ". Now we can use " (code "vowels") " to refer to the set of vowels. "] - [:p.bottom - "Now, we have a way to find out if a character is a vowel, now we just need a way to remove everything that isn't " - "a vowel from our teddybear string. I have just the function! We need " (code "filter") ". Filter takes what " - "is called a 'predicate', that is, a function that returns true or false, and it applies this function to each " - "element of a sequence in turn. If the predicate function returns false or nil for an element, that element is " - "removed from the sequence. If the predicate function returns anything that isn't false or nil for an element, " - "that element is left alone. In Clojure, anything that isn't false or nil is considered a true value."] - [:p.bottom - "Let's try filter out a bit. Let's try to filter out all odd numbers from a sequence of numbers. Clojure has " - "a function called " (code "odd?") " that we can use. Putting a question mark at the end is a Clojure naming convention " - "for functions that are predicates (return either true or false). Try it out in the REPL:"] - (code "(odd? 1)\n") - (code "(odd? 2)") - [:p.bottom - "Okay, now we need a sequence of numbers. We could type these out by hand, but that's tedious, and as Clojure " - "programmers, we do not tolerate 'tedious'. We can use Clojure's range function to generate these numbers for us. " - (code "range") ", if given one integer, will generate a sequence of numbers from 0 to the integer that you passed to " - "it. Try this: " (code "(range 10)") ". We have a sequence of numbers from 0 to 9. Apparently, the upper-bound " - "(the number you passed to range) is 'inclusive', meaning it's not included in the resulting range. If we really " - "wanted a sequence of numbers from 0 to 10, we'd do this: " (code "(range 11)") "."] - [:p.bottom - "We don't really want the zero, so let's give range a lower-bound, along with an upper-bound. It can take two " - "arguments: " (code "(range 1 11)") ". There we go! We have all of the numbers from 1 to 10. Now, let's use " - "filter and the odd? function to get all of the odd numbers out of the sequence. Try this: " - (code "(filter odd? (range 1 11))") ". Ka-chow! That's magic, isn't it? Work of art, that is."] - [:p.bottom - "Now that we know how to use filter, we can now use it to filter out all of the vowels in our teddy string. " - "We have our string defined in teddy, and our set of vowels in vowels, so we should try this: " - (code "(filter vowels teddy)") - ". That was easy enough. Now we have a sequence of characters. But we still need to count them! Luckily, " - "Clojure has just the function for that: " (code "count") "! It takes a sequence and counts (whoda thunk it) the " - "number of elements in the sequence. Let's try it out: " (code "(count (filter vowels teddy))") ". Yeehaw!" - " You did it! Once again, you never cease to amaze me. You're catching on quickly."] - [:p.bottom "That's the end of step 4. Press Next to move on."])) - -(def tutorial - (html - [:div#tuttext - tutorial0-text] - [:div.continue - [:input#back {:type "button" :value "Back"}] - [:input#continue {:type "button" :value "Next"}]])) - -(defn get-tutorial [step] - (condp = step - "0" tutorial - "1" tutorial0-text - "2" tutorial1-text - "3" tutorial2-text - "4" tutorial3-text - "5" tutorial4-text - "6" "TODO!")) \ No newline at end of file diff --git a/src/tryclojure/views/eval.clj b/src/tryclojure/views/eval.clj new file mode 100644 index 0000000..a8a312d --- /dev/null +++ b/src/tryclojure/views/eval.clj @@ -0,0 +1,15 @@ +(ns tryclojure.views.eval + (:require [tryclojure.models.eval :refer [eval-request]] + [noir.response :as resp])) + +(defn eval-json [expr jsonp] + (let [{:keys [expr result error message] :as res} (eval-request expr) + data (if error + res + (let [[out res] result] + {:expr (pr-str expr) + :result (str out (pr-str res))}))] + + (if jsonp + (resp/jsonp jsonp data) + (resp/json data)))) \ No newline at end of file diff --git a/src/tryclojure/views/home.clj b/src/tryclojure/views/home.clj new file mode 100644 index 0000000..d717b7e --- /dev/null +++ b/src/tryclojure/views/home.clj @@ -0,0 +1,77 @@ +(ns tryclojure.views.home + (:require [hiccup.element :refer [javascript-tag link-to unordered-list]] + [hiccup.page :refer [include-css include-js html5]] + [hiccup.core :refer [html]])) + +(defn links-html [] + (html + (unordered-list + [(link-to "http://clojure.org" "The official Clojure website") + (link-to "http://clojure-doc.org/" "Clojure tutorials and documentation") + (link-to "http://groups.google.com/group/clojure" "Clojure mailing list") + (link-to "http://joyofclojure.com/" "The Joy of Clojure: a book by Michael Fogus and Chris Houser") + (link-to "http://disclojure.org" "Disclojure") + (link-to "http://planet.clojure.in" "Planet Clojure")]))) + +(defn about-html [] + (html + [:p.bottom + "Welcome to Try Clojure - a quick tour of Clojure for absolute beginners."] + [:p.bottom + "Here is our only disclaimer: this site is an introduction to Clojure, not a generic Clojure REPL. " + "You won't be able to do everything in it that you could do in your local interpreter. " + "Also, the interpreter deletes the data that you enter if you define too many things, or after 15 minutes."] + [:p.bottom + "TryClojure is written in Clojure and JavaScript with " + (link-to "https://github.com/weavejester/compojure" "Compojure") ", " + (link-to "https://github.com/noir-clojure/lib-noir" "lib-noir") ", " + (link-to "https://github.com/flatland/clojail" "clojail") ", and Chris Done's " + (link-to "https://github.com/chrisdone/jquery-console" "jquery-console") ". " + " The design is by " (link-to "http://apgwoz.com" "Andrew Gwozdziewycz") "."])) + +(defn home-html [] + (html + [:p.bottom + "Welcome to Clojure! " + "You can see a Clojure interpreter above - we call it a REPL."] + [:p.bottom "Type next in the REPL to begin." ])) + +(defn root-html [] + (html5 + [:head + [:meta {:name "viewport" :content "width=device-width, initial-scale=1"}] + (include-css "/css/tryclojure.css" + "/css/gh-fork-ribbon.css") + (include-js "http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" + "/javascript/jquery-console/jquery.console.js" + "/javascript/tryclojure.js") + [:title "Try Clojure"]] + [:body + [:div#wrapper + [:div.github-fork-ribbon-wrapper.right + [:div.github-fork-ribbon + (link-to "https://github.com/Raynes/tryclojure" "Fork me on GitHub")]] + [:div#content + [:div#header + [:h1 + [:span.logo-try "Try"] " " + [:span.logo-clojure "Clo" [:em "j"] "ure"]]] + [:div#container + [:div#console.console] + [:div#buttons + [:a#links.buttons "links"] + [:a#about.buttons.last "about"]] + [:div#changer (home-html)]] + [:div.footer + [:p.bottom "©2011-2012 Anthony Grimes and numerous contributors."]] + (javascript-tag + "var _gaq = _gaq || []; + _gaq.push(['_setAccount', 'UA-27340918-1']); + _gaq.push(['_trackPageview']); + + (function() { + var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; + ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); + })();")]]])) + diff --git a/src/tryclojure/views/tutorial.clj b/src/tryclojure/views/tutorial.clj new file mode 100644 index 0000000..901fe52 --- /dev/null +++ b/src/tryclojure/views/tutorial.clj @@ -0,0 +1,4 @@ +(ns tryclojure.views.tutorial) + +(defn tutorial-html [page] + (slurp (str "resources/public/tutorial/" page ".html"))) \ No newline at end of file diff --git a/test/tryclojure/core_test.clj b/test/tryclojure/core_test.clj deleted file mode 100644 index d91e716..0000000 --- a/test/tryclojure/core_test.clj +++ /dev/null @@ -1,6 +0,0 @@ -(ns tryclojure.core-test - (:use [tryclojure.core] :reload-all) - (:use [clojure.test])) - -(deftest replace-me ;; FIXME: write - (is false)) diff --git a/test/tryclojure/eval_test.clj b/test/tryclojure/eval_test.clj new file mode 100644 index 0000000..041ae6c --- /dev/null +++ b/test/tryclojure/eval_test.clj @@ -0,0 +1,18 @@ +(ns tryclojure.eval-test + (:use tryclojure.models.eval + clojure.test) + (:require noir.session)) + +(def sb (make-sandbox)) + +(deftest eval-form-test + (let [form "(do (println 10) (+ 3 3))" + result (eval-string form sb)] + (is (= "10\n" (-> result :result first str))) + (is (= "6" (-> result :result second str))) + (is (= (read-string form) (-> result :expr))))) + +(alter-var-root #'noir.session/*noir-session* (constantly (atom {}))) + +(deftest eval-request-test + (is (= "Execution Timed Out!" (:message (eval-request "(while true)"))))) \ No newline at end of file