├── .gitattributes ├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── LICENSE ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml ├── resources └── console-screenshot.png └── src ├── main ├── java │ └── com │ │ └── github │ │ └── gaborbata │ │ └── console │ │ ├── GroovyWebConsoleApplication.java │ │ └── controller │ │ └── GroovyConsoleController.java └── resources │ ├── application.properties │ └── public │ └── console │ ├── css │ ├── ambiance.css │ ├── codemirror.css │ └── console.css │ ├── examples │ ├── get-environment-info.groovy │ ├── is-it-friday.groovy │ └── list-spring-beans.groovy │ ├── images │ └── console.png │ ├── index.html │ └── js │ ├── codemirror.js │ ├── console.js │ ├── groovy.js │ ├── javascript.js │ ├── jquery-3.3.1.min.js │ ├── jquery.blockui.js │ ├── lz-string.js │ └── matchbrackets.js └── test └── java └── com └── github └── gaborbata └── console ├── configuration └── JGivenConfig.java └── controller ├── GroovyConsoleControllerTest.java └── stage └── GroovyConsoleControllerStage.java /.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-vendored=false 2 | src/main/resources/**/* linguist-vendored 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaborbata/groovy-web-console/32e6ea4686605472f90e3045f13f2e2fc80b2692/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.2/apache-maven-3.5.2-bin.zip 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Gabor Bata 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Groovy Web Console 2 | 3 | Overview 4 | -------- 5 | This application is intended for embedding a Groovy console in a Spring Boot web application 6 | which then could access the application context (for testing purposes). 7 | 8 | ![Groovy Web Console](https://raw.githubusercontent.com/gabor-bata/groovy-web-console/master/resources/console-screenshot.png) 9 | 10 | How to compile 11 | -------------- 12 | 13 | mvnw clean package 14 | 15 | Usage 16 | ----- 17 | Java 17 or later is required to start the application. 18 | 19 | * Start server: `mvnw spring-boot:run` or `java -jar target/groovy-web-console-1.0.0-SNAPSHOT.jar` 20 | * Access the console: `http://localhost:8080/console` 21 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Migwn, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 204 | echo $MAVEN_PROJECTBASEDIR 205 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 206 | 207 | # For Cygwin, switch paths to Windows format before running java 208 | if $cygwin; then 209 | [ -n "$M2_HOME" ] && 210 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 211 | [ -n "$JAVA_HOME" ] && 212 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 213 | [ -n "$CLASSPATH" ] && 214 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 215 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 216 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 217 | fi 218 | 219 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 220 | 221 | exec "$JAVACMD" \ 222 | $MAVEN_OPTS \ 223 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 224 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 225 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 226 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 84 | @REM Fallback to current working directory if not found. 85 | 86 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 87 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 88 | 89 | set EXEC_DIR=%CD% 90 | set WDIR=%EXEC_DIR% 91 | :findBaseDir 92 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 93 | cd .. 94 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 95 | set WDIR=%CD% 96 | goto findBaseDir 97 | 98 | :baseDirFound 99 | set MAVEN_PROJECTBASEDIR=%WDIR% 100 | cd "%EXEC_DIR%" 101 | goto endDetectBaseDir 102 | 103 | :baseDirNotFound 104 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 105 | cd "%EXEC_DIR%" 106 | 107 | :endDetectBaseDir 108 | 109 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 110 | 111 | @setlocal EnableExtensions EnableDelayedExpansion 112 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 113 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 114 | 115 | :endReadAdditionalConfig 116 | 117 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 118 | 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 123 | if ERRORLEVEL 1 goto error 124 | goto end 125 | 126 | :error 127 | set ERROR_CODE=1 128 | 129 | :end 130 | @endlocal & set ERROR_CODE=%ERROR_CODE% 131 | 132 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 133 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 134 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 135 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 136 | :skipRcPost 137 | 138 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 139 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 140 | 141 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 142 | 143 | exit /B %ERROR_CODE% 144 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.github.gaborbata 6 | groovy-web-console 7 | 1.0.0-SNAPSHOT 8 | jar 9 | 10 | groovy-web-console 11 | Groovy web console 12 | 13 | 14 | org.springframework.boot 15 | spring-boot-starter-parent 16 | 3.0.1 17 | 18 | 19 | 20 | 21 | UTF-8 22 | UTF-8 23 | 17 24 | 4.0.7 25 | 1.2.4 26 | 0.8.8 27 | 3.0.1 28 | 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-web 34 | 35 | 36 | 37 | org.apache.groovy 38 | groovy-all 39 | ${groovy.version} 40 | pom 41 | 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-starter-test 46 | test 47 | 48 | 49 | com.tngtech.jgiven 50 | jgiven-junit5 51 | ${jgiven.version} 52 | test 53 | 54 | 55 | com.tngtech.jgiven 56 | jgiven-spring-junit5 57 | ${jgiven.version} 58 | test 59 | 60 | 61 | 62 | 63 | 64 | 65 | org.springframework.boot 66 | spring-boot-maven-plugin 67 | 68 | 69 | org.jacoco 70 | jacoco-maven-plugin 71 | ${jacoco.version} 72 | 73 | 74 | 75 | prepare-agent 76 | 77 | 78 | 79 | jacoco-report 80 | prepare-package 81 | 82 | report 83 | 84 | 85 | 86 | 87 | 88 | com.tngtech.jgiven 89 | jgiven-maven-plugin 90 | ${jgiven.version} 91 | 92 | 93 | jgiven-report 94 | prepare-package 95 | 96 | report 97 | 98 | 99 | 100 | 101 | html 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /resources/console-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaborbata/groovy-web-console/32e6ea4686605472f90e3045f13f2e2fc80b2692/resources/console-screenshot.png -------------------------------------------------------------------------------- /src/main/java/com/github/gaborbata/console/GroovyWebConsoleApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.gaborbata.console; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 6 | 7 | @SpringBootApplication 8 | @ConditionalOnProperty(name = "groovy-web-console.application-enabled", havingValue = "true") 9 | public class GroovyWebConsoleApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(GroovyWebConsoleApplication.class, args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/gaborbata/console/controller/GroovyConsoleController.java: -------------------------------------------------------------------------------- 1 | package com.github.gaborbata.console.controller; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import groovy.lang.Binding; 5 | import groovy.lang.GroovyShell; 6 | import groovy.transform.TimedInterrupt; 7 | import org.codehaus.groovy.control.CompilerConfiguration; 8 | import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer; 9 | import org.codehaus.groovy.control.customizers.SecureASTCustomizer; 10 | import org.springframework.beans.BeansException; 11 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 12 | import org.springframework.context.ApplicationContext; 13 | import org.springframework.context.ApplicationContextAware; 14 | import org.springframework.stereotype.Controller; 15 | import org.springframework.util.StringUtils; 16 | import org.springframework.web.bind.annotation.RequestMapping; 17 | import org.springframework.web.bind.annotation.RequestMethod; 18 | import org.springframework.web.bind.annotation.RequestParam; 19 | import org.springframework.web.bind.annotation.ResponseBody; 20 | 21 | import java.io.ByteArrayOutputStream; 22 | import java.io.OutputStream; 23 | import java.io.PrintStream; 24 | import java.util.List; 25 | import java.util.concurrent.CompletableFuture; 26 | import java.util.stream.Collectors; 27 | import java.util.stream.Stream; 28 | 29 | import static java.util.Collections.singletonMap; 30 | 31 | /** 32 | * Controller for evaluating scripts from groovy console. 33 | */ 34 | @Controller 35 | @RequestMapping("/console") 36 | @ConditionalOnProperty(name = "groovy-web-console.enabled", havingValue = "true", matchIfMissing = true) 37 | public class GroovyConsoleController implements ApplicationContextAware { 38 | 39 | private static final long SCRIPT_TIMEOUT_IN_SECONDS = 5; 40 | private static final List RECEIVERS_BLACK_LIST = Stream.of(System.class, Thread.class) 41 | .map(Class::getName) 42 | .collect(Collectors.toList()); 43 | 44 | private ApplicationContext applicationContext; 45 | 46 | /** 47 | * Redirects to groovy console index page. 48 | * 49 | * @return the redirect view of groovy console index page 50 | */ 51 | @RequestMapping(method = RequestMethod.GET) 52 | public String index() { 53 | return "redirect:/console/index.html"; 54 | } 55 | 56 | /** 57 | * Executes the given groovy script 58 | * 59 | * @param script the groovy script 60 | * @return the result object 61 | */ 62 | @RequestMapping(value = "/groovy", method = {RequestMethod.GET, RequestMethod.POST}) 63 | @ResponseBody 64 | public CompletableFuture execute(@RequestParam String script) { 65 | return CompletableFuture.supplyAsync(() -> { 66 | var out = new ByteArrayOutputStream(); 67 | var groovyShell = createGroovyShell(out); 68 | var result = groovyShell.evaluate(script); 69 | return ScriptResult.create(result, out.toString()); 70 | }).exceptionally(ScriptResult::create); 71 | } 72 | 73 | @Override 74 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 75 | this.applicationContext = applicationContext; 76 | } 77 | 78 | private GroovyShell createGroovyShell(OutputStream outputStream) { 79 | var configuration = createCompilerConfiguration(); 80 | var binding = createBinding(outputStream); 81 | return new GroovyShell(binding, configuration); 82 | } 83 | 84 | private Binding createBinding(OutputStream outputStream) { 85 | var binding = new Binding(); 86 | binding.setVariable("applicationContext", applicationContext); 87 | binding.setProperty("out", new PrintStream(outputStream, true)); 88 | return binding; 89 | } 90 | 91 | private CompilerConfiguration createCompilerConfiguration() { 92 | var timedCustomizer = new ASTTransformationCustomizer(singletonMap("value", SCRIPT_TIMEOUT_IN_SECONDS), TimedInterrupt.class); 93 | var secureCustomizer = new SecureASTCustomizer(); 94 | secureCustomizer.setReceiversBlackList(RECEIVERS_BLACK_LIST); 95 | var configuration = new CompilerConfiguration(); 96 | configuration.addCompilationCustomizers(secureCustomizer, timedCustomizer); 97 | return configuration; 98 | } 99 | 100 | @JsonInclude(JsonInclude.Include.NON_NULL) 101 | private static final class ScriptResult { 102 | 103 | private String[] output; 104 | private Object result; 105 | 106 | private ScriptResult() { 107 | } 108 | 109 | public String[] getOutput() { 110 | return output; 111 | } 112 | 113 | public Object getResult() { 114 | return result; 115 | } 116 | 117 | private static ScriptResult create(Throwable throwable) { 118 | var message = throwable.getMessage() == null ? throwable.getClass().getName() : throwable.getMessage(); 119 | return create(null, message); 120 | } 121 | 122 | private static ScriptResult create(Object result, String output) { 123 | var scriptletResult = new ScriptResult(); 124 | scriptletResult.result = result; 125 | if (StringUtils.hasLength(output)) { 126 | scriptletResult.output = output.split(System.lineSeparator()); 127 | } 128 | return scriptletResult; 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name: Groovy Web Console 2 | groovy-web-console.application-enabled=true 3 | -------------------------------------------------------------------------------- /src/main/resources/public/console/css/ambiance.css: -------------------------------------------------------------------------------- 1 | /* ambiance theme for codemirror */ 2 | 3 | /* Color scheme */ 4 | 5 | .cm-s-ambiance .cm-header { color: blue; } 6 | .cm-s-ambiance .cm-quote { color: #24C2C7; } 7 | 8 | .cm-s-ambiance .cm-keyword { color: #cda869; } 9 | .cm-s-ambiance .cm-atom { color: #CF7EA9; } 10 | .cm-s-ambiance .cm-number { color: #78CF8A; } 11 | .cm-s-ambiance .cm-def { color: #aac6e3; } 12 | .cm-s-ambiance .cm-variable { color: #ffb795; } 13 | .cm-s-ambiance .cm-variable-2 { color: #eed1b3; } 14 | .cm-s-ambiance .cm-variable-3, .cm-s-ambiance .cm-type { color: #faded3; } 15 | .cm-s-ambiance .cm-property { color: #eed1b3; } 16 | .cm-s-ambiance .cm-operator { color: #fa8d6a; } 17 | .cm-s-ambiance .cm-comment { color: #555; font-style:italic; } 18 | .cm-s-ambiance .cm-string { color: #8f9d6a; } 19 | .cm-s-ambiance .cm-string-2 { color: #9d937c; } 20 | .cm-s-ambiance .cm-meta { color: #D2A8A1; } 21 | .cm-s-ambiance .cm-qualifier { color: yellow; } 22 | .cm-s-ambiance .cm-builtin { color: #9999cc; } 23 | .cm-s-ambiance .cm-bracket { color: #24C2C7; } 24 | .cm-s-ambiance .cm-tag { color: #fee4ff; } 25 | .cm-s-ambiance .cm-attribute { color: #9B859D; } 26 | .cm-s-ambiance .cm-hr { color: pink; } 27 | .cm-s-ambiance .cm-link { color: #F4C20B; } 28 | .cm-s-ambiance .cm-special { color: #FF9D00; } 29 | .cm-s-ambiance .cm-error { color: #AF2018; } 30 | 31 | .cm-s-ambiance .CodeMirror-matchingbracket { color: #0f0; } 32 | .cm-s-ambiance .CodeMirror-nonmatchingbracket { color: #f22; } 33 | 34 | .cm-s-ambiance div.CodeMirror-selected { background: rgba(255, 255, 255, 0.15); } 35 | .cm-s-ambiance.CodeMirror-focused div.CodeMirror-selected { background: rgba(255, 255, 255, 0.10); } 36 | .cm-s-ambiance .CodeMirror-line::selection, .cm-s-ambiance .CodeMirror-line > span::selection, .cm-s-ambiance .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); } 37 | .cm-s-ambiance .CodeMirror-line::-moz-selection, .cm-s-ambiance .CodeMirror-line > span::-moz-selection, .cm-s-ambiance .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); } 38 | 39 | /* Editor styling */ 40 | 41 | .cm-s-ambiance.CodeMirror { 42 | line-height: 1.40em; 43 | color: #E6E1DC; 44 | background-color: #202020; 45 | -webkit-box-shadow: inset 0 0 10px black; 46 | -moz-box-shadow: inset 0 0 10px black; 47 | box-shadow: inset 0 0 10px black; 48 | } 49 | 50 | .cm-s-ambiance .CodeMirror-gutters { 51 | background: #3D3D3D; 52 | border-right: 1px solid #4D4D4D; 53 | box-shadow: 0 10px 20px black; 54 | } 55 | 56 | .cm-s-ambiance .CodeMirror-linenumber { 57 | text-shadow: 0px 1px 1px #4d4d4d; 58 | color: #111; 59 | padding: 0 5px; 60 | } 61 | 62 | .cm-s-ambiance .CodeMirror-guttermarker { color: #aaa; } 63 | .cm-s-ambiance .CodeMirror-guttermarker-subtle { color: #111; } 64 | 65 | .cm-s-ambiance .CodeMirror-cursor { border-left: 1px solid #7991E8; } 66 | 67 | .cm-s-ambiance .CodeMirror-activeline-background { 68 | background: none repeat scroll 0% 0% rgba(255, 255, 255, 0.031); 69 | } 70 | 71 | .cm-s-ambiance.CodeMirror, 72 | .cm-s-ambiance .CodeMirror-gutters { 73 | background-image: url("data:image/png;base64,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"); 74 | } 75 | -------------------------------------------------------------------------------- /src/main/resources/public/console/css/codemirror.css: -------------------------------------------------------------------------------- 1 | /* BASICS */ 2 | 3 | .CodeMirror { 4 | /* Set height, width, borders, and global font properties here */ 5 | font-family: monospace; 6 | height: 300px; 7 | color: black; 8 | direction: ltr; 9 | } 10 | 11 | /* PADDING */ 12 | 13 | .CodeMirror-lines { 14 | padding: 4px 0; /* Vertical padding around content */ 15 | } 16 | .CodeMirror pre { 17 | padding: 0 4px; /* Horizontal padding of content */ 18 | } 19 | 20 | .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 21 | background-color: white; /* The little square between H and V scrollbars */ 22 | } 23 | 24 | /* GUTTER */ 25 | 26 | .CodeMirror-gutters { 27 | border-right: 1px solid #ddd; 28 | background-color: #f7f7f7; 29 | white-space: nowrap; 30 | } 31 | .CodeMirror-linenumbers {} 32 | .CodeMirror-linenumber { 33 | padding: 0 3px 0 5px; 34 | min-width: 20px; 35 | text-align: right; 36 | color: #999; 37 | white-space: nowrap; 38 | } 39 | 40 | .CodeMirror-guttermarker { color: black; } 41 | .CodeMirror-guttermarker-subtle { color: #999; } 42 | 43 | /* CURSOR */ 44 | 45 | .CodeMirror-cursor { 46 | border-left: 1px solid black; 47 | border-right: none; 48 | width: 0; 49 | } 50 | /* Shown when moving in bi-directional text */ 51 | .CodeMirror div.CodeMirror-secondarycursor { 52 | border-left: 1px solid silver; 53 | } 54 | .cm-fat-cursor .CodeMirror-cursor { 55 | width: auto; 56 | border: 0 !important; 57 | background: #7e7; 58 | } 59 | .cm-fat-cursor div.CodeMirror-cursors { 60 | z-index: 1; 61 | } 62 | .cm-fat-cursor-mark { 63 | background-color: rgba(20, 255, 20, 0.5); 64 | -webkit-animation: blink 1.06s steps(1) infinite; 65 | -moz-animation: blink 1.06s steps(1) infinite; 66 | animation: blink 1.06s steps(1) infinite; 67 | } 68 | .cm-animate-fat-cursor { 69 | width: auto; 70 | border: 0; 71 | -webkit-animation: blink 1.06s steps(1) infinite; 72 | -moz-animation: blink 1.06s steps(1) infinite; 73 | animation: blink 1.06s steps(1) infinite; 74 | background-color: #7e7; 75 | } 76 | @-moz-keyframes blink { 77 | 0% {} 78 | 50% { background-color: transparent; } 79 | 100% {} 80 | } 81 | @-webkit-keyframes blink { 82 | 0% {} 83 | 50% { background-color: transparent; } 84 | 100% {} 85 | } 86 | @keyframes blink { 87 | 0% {} 88 | 50% { background-color: transparent; } 89 | 100% {} 90 | } 91 | 92 | /* Can style cursor different in overwrite (non-insert) mode */ 93 | .CodeMirror-overwrite .CodeMirror-cursor {} 94 | 95 | .cm-tab { display: inline-block; text-decoration: inherit; } 96 | 97 | .CodeMirror-rulers { 98 | position: absolute; 99 | left: 0; right: 0; top: -50px; bottom: -20px; 100 | overflow: hidden; 101 | } 102 | .CodeMirror-ruler { 103 | border-left: 1px solid #ccc; 104 | top: 0; bottom: 0; 105 | position: absolute; 106 | } 107 | 108 | /* DEFAULT THEME */ 109 | 110 | .cm-s-default .cm-header {color: blue;} 111 | .cm-s-default .cm-quote {color: #090;} 112 | .cm-negative {color: #d44;} 113 | .cm-positive {color: #292;} 114 | .cm-header, .cm-strong {font-weight: bold;} 115 | .cm-em {font-style: italic;} 116 | .cm-link {text-decoration: underline;} 117 | .cm-strikethrough {text-decoration: line-through;} 118 | 119 | .cm-s-default .cm-keyword {color: #708;} 120 | .cm-s-default .cm-atom {color: #219;} 121 | .cm-s-default .cm-number {color: #164;} 122 | .cm-s-default .cm-def {color: #00f;} 123 | .cm-s-default .cm-variable, 124 | .cm-s-default .cm-punctuation, 125 | .cm-s-default .cm-property, 126 | .cm-s-default .cm-operator {} 127 | .cm-s-default .cm-variable-2 {color: #05a;} 128 | .cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;} 129 | .cm-s-default .cm-comment {color: #a50;} 130 | .cm-s-default .cm-string {color: #a11;} 131 | .cm-s-default .cm-string-2 {color: #f50;} 132 | .cm-s-default .cm-meta {color: #555;} 133 | .cm-s-default .cm-qualifier {color: #555;} 134 | .cm-s-default .cm-builtin {color: #30a;} 135 | .cm-s-default .cm-bracket {color: #997;} 136 | .cm-s-default .cm-tag {color: #170;} 137 | .cm-s-default .cm-attribute {color: #00c;} 138 | .cm-s-default .cm-hr {color: #999;} 139 | .cm-s-default .cm-link {color: #00c;} 140 | 141 | .cm-s-default .cm-error {color: #f00;} 142 | .cm-invalidchar {color: #f00;} 143 | 144 | .CodeMirror-composing { border-bottom: 2px solid; } 145 | 146 | /* Default styles for common addons */ 147 | 148 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;} 149 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;} 150 | .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } 151 | .CodeMirror-activeline-background {background: #e8f2ff;} 152 | 153 | /* STOP */ 154 | 155 | /* The rest of this file contains styles related to the mechanics of 156 | the editor. You probably shouldn't touch them. */ 157 | 158 | .CodeMirror { 159 | position: relative; 160 | overflow: hidden; 161 | background: white; 162 | } 163 | 164 | .CodeMirror-scroll { 165 | overflow: scroll !important; /* Things will break if this is overridden */ 166 | /* 30px is the magic margin used to hide the element's real scrollbars */ 167 | /* See overflow: hidden in .CodeMirror */ 168 | margin-bottom: -30px; margin-right: -30px; 169 | padding-bottom: 30px; 170 | height: 100%; 171 | outline: none; /* Prevent dragging from highlighting the element */ 172 | position: relative; 173 | } 174 | .CodeMirror-sizer { 175 | position: relative; 176 | border-right: 30px solid transparent; 177 | } 178 | 179 | /* The fake, visible scrollbars. Used to force redraw during scrolling 180 | before actual scrolling happens, thus preventing shaking and 181 | flickering artifacts. */ 182 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 183 | position: absolute; 184 | z-index: 6; 185 | display: none; 186 | } 187 | .CodeMirror-vscrollbar { 188 | right: 0; top: 0; 189 | overflow-x: hidden; 190 | overflow-y: scroll; 191 | } 192 | .CodeMirror-hscrollbar { 193 | bottom: 0; left: 0; 194 | overflow-y: hidden; 195 | overflow-x: scroll; 196 | } 197 | .CodeMirror-scrollbar-filler { 198 | right: 0; bottom: 0; 199 | } 200 | .CodeMirror-gutter-filler { 201 | left: 0; bottom: 0; 202 | } 203 | 204 | .CodeMirror-gutters { 205 | position: absolute; left: 0; top: 0; 206 | min-height: 100%; 207 | z-index: 3; 208 | } 209 | .CodeMirror-gutter { 210 | white-space: normal; 211 | height: 100%; 212 | display: inline-block; 213 | vertical-align: top; 214 | margin-bottom: -30px; 215 | } 216 | .CodeMirror-gutter-wrapper { 217 | position: absolute; 218 | z-index: 4; 219 | background: none !important; 220 | border: none !important; 221 | } 222 | .CodeMirror-gutter-background { 223 | position: absolute; 224 | top: 0; bottom: 0; 225 | z-index: 4; 226 | } 227 | .CodeMirror-gutter-elt { 228 | position: absolute; 229 | cursor: default; 230 | z-index: 4; 231 | } 232 | .CodeMirror-gutter-wrapper ::selection { background-color: transparent } 233 | .CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent } 234 | 235 | .CodeMirror-lines { 236 | cursor: text; 237 | min-height: 1px; /* prevents collapsing before first draw */ 238 | } 239 | .CodeMirror pre { 240 | /* Reset some styles that the rest of the page might have set */ 241 | -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; 242 | border-width: 0; 243 | background: transparent; 244 | font-family: inherit; 245 | font-size: inherit; 246 | margin: 0; 247 | white-space: pre; 248 | word-wrap: normal; 249 | line-height: inherit; 250 | color: inherit; 251 | z-index: 2; 252 | position: relative; 253 | overflow: visible; 254 | -webkit-tap-highlight-color: transparent; 255 | -webkit-font-variant-ligatures: contextual; 256 | font-variant-ligatures: contextual; 257 | } 258 | .CodeMirror-wrap pre { 259 | word-wrap: break-word; 260 | white-space: pre-wrap; 261 | word-break: normal; 262 | } 263 | 264 | .CodeMirror-linebackground { 265 | position: absolute; 266 | left: 0; right: 0; top: 0; bottom: 0; 267 | z-index: 0; 268 | } 269 | 270 | .CodeMirror-linewidget { 271 | position: relative; 272 | z-index: 2; 273 | padding: 0.1px; /* Force widget margins to stay inside of the container */ 274 | } 275 | 276 | .CodeMirror-widget {} 277 | 278 | .CodeMirror-rtl pre { direction: rtl; } 279 | 280 | .CodeMirror-code { 281 | outline: none; 282 | } 283 | 284 | /* Force content-box sizing for the elements where we expect it */ 285 | .CodeMirror-scroll, 286 | .CodeMirror-sizer, 287 | .CodeMirror-gutter, 288 | .CodeMirror-gutters, 289 | .CodeMirror-linenumber { 290 | -moz-box-sizing: content-box; 291 | box-sizing: content-box; 292 | } 293 | 294 | .CodeMirror-measure { 295 | position: absolute; 296 | width: 100%; 297 | height: 0; 298 | overflow: hidden; 299 | visibility: hidden; 300 | } 301 | 302 | .CodeMirror-cursor { 303 | position: absolute; 304 | pointer-events: none; 305 | } 306 | .CodeMirror-measure pre { position: static; } 307 | 308 | div.CodeMirror-cursors { 309 | visibility: hidden; 310 | position: relative; 311 | z-index: 3; 312 | } 313 | div.CodeMirror-dragcursors { 314 | visibility: visible; 315 | } 316 | 317 | .CodeMirror-focused div.CodeMirror-cursors { 318 | visibility: visible; 319 | } 320 | 321 | .CodeMirror-selected { background: #d9d9d9; } 322 | .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } 323 | .CodeMirror-crosshair { cursor: crosshair; } 324 | .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } 325 | .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } 326 | 327 | .cm-searching { 328 | background-color: #ffa; 329 | background-color: rgba(255, 255, 0, .4); 330 | } 331 | 332 | /* Used to force a border model for a node */ 333 | .cm-force-border { padding-right: .1px; } 334 | 335 | @media print { 336 | /* Hide the cursor when printing */ 337 | .CodeMirror div.CodeMirror-cursors { 338 | visibility: hidden; 339 | } 340 | } 341 | 342 | /* See issue #2901 */ 343 | .cm-tab-wrap-hack:after { content: ''; } 344 | 345 | /* Help users use markselection to safely style text background */ 346 | span.CodeMirror-selectedtext { background: none; } 347 | -------------------------------------------------------------------------------- /src/main/resources/public/console/css/console.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #4d4d4d; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | .CodeMirror { 8 | font-size: 14px; 9 | } 10 | 11 | .blockUI { 12 | font-family: sans-serif; 13 | } 14 | 15 | .input-code-title img { 16 | position: absolute; 17 | top: 1px; 18 | } 19 | 20 | .input-code-title .title { 21 | margin-left: 40px; 22 | } 23 | 24 | .input-code-title button { 25 | background-color: #5f5f5f; 26 | font-family: sans-serif; 27 | font-size: 14px; 28 | height: 22px; 29 | color: #fff; 30 | border: 1px solid #ddd; 31 | margin-left: 20px; 32 | } 33 | 34 | .input-code-title .pulled-right { 35 | float: right; 36 | } 37 | 38 | .input-code-title a { 39 | color: #fff; 40 | } 41 | 42 | .input-code-title select { 43 | background-color: #5f5f5f; 44 | font-family: sans-serif; 45 | font-size: 14px; 46 | height: 22px; 47 | color: #fff; 48 | border: 1px solid #ddd; 49 | margin-right: 20px; 50 | } 51 | 52 | .input-code-title, .result-code-title { 53 | background-color: #4d4d4d; 54 | border-top: 1px solid #000; 55 | border-bottom: 1px solid #000; 56 | font-family: sans-serif; 57 | font-size: 14px; 58 | color: #fff; 59 | padding: 6px; 60 | } 61 | -------------------------------------------------------------------------------- /src/main/resources/public/console/examples/get-environment-info.groovy: -------------------------------------------------------------------------------- 1 | // Get Spring Boot environment information 2 | def environment = applicationContext.getBean("environment") 3 | println "Active profiles: ${environment.getActiveProfiles()}" 4 | println "Application name: ${environment.getProperty('spring.application.name')}" 5 | -------------------------------------------------------------------------------- /src/main/resources/public/console/examples/is-it-friday.groovy: -------------------------------------------------------------------------------- 1 | // checks if groovy console works 2 | def calendar = Calendar.getInstance() 3 | def isFriday = calendar.get(Calendar.DAY_OF_WEEK) == Calendar.FRIDAY 4 | println "Is it friday? ${isFriday ? 'Yes' : 'No'}" 5 | -------------------------------------------------------------------------------- /src/main/resources/public/console/examples/list-spring-beans.groovy: -------------------------------------------------------------------------------- 1 | // list all spring beans 2 | applicationContext.getBeanDefinitionNames() 3 | -------------------------------------------------------------------------------- /src/main/resources/public/console/images/console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaborbata/groovy-web-console/32e6ea4686605472f90e3045f13f2e2fc80b2692/src/main/resources/public/console/images/console.png -------------------------------------------------------------------------------- /src/main/resources/public/console/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Groovy Web Console 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | Edit code 16 | 17 | 18 | 19 | 20 | 26 | 27 |
28 | 29 |
Result
30 | 31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/main/resources/public/console/js/console.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var $location = $(location); 3 | var $window = $(window); 4 | var $exampleSelector = $("#input-code-example-select"); 5 | 6 | function hashChangeEventHandler() { 7 | var hash = $location.attr('hash'); 8 | if (hash) { 9 | var decompressed = LZString.decompressFromEncodedURIComponent(hash.substring(1)); 10 | if (decompressed) { 11 | inputEditor.getDoc().setValue(decompressed); 12 | } 13 | $exampleSelector.val(""); 14 | } else { 15 | $exampleSelector.val("/console/examples/get-environment-info.groovy"); 16 | } 17 | $exampleSelector.trigger("change"); 18 | } 19 | 20 | var inputEditor = CodeMirror.fromTextArea(document.getElementById("input-code"), { 21 | mode: "text/x-groovy", 22 | lineNumbers: true, 23 | theme: "ambiance", 24 | matchBrackets: true, 25 | indentUnit: 2 26 | }); 27 | 28 | var resultEditor = CodeMirror.fromTextArea(document.getElementById("result-code"), { 29 | mode: "application/x-json", 30 | lineNumbers: true, 31 | theme: "ambiance", 32 | matchBrackets: true, 33 | indentUnit: 2, 34 | readOnly: true 35 | }); 36 | 37 | $("#send-button").on("click", function() { 38 | $.blockUI(); 39 | $.ajax({ 40 | url: "/console/groovy", 41 | type: "POST", 42 | data: { script: inputEditor.getValue() } 43 | }).done(function(data) { 44 | resultEditor.getDoc().setValue(JSON.stringify(data, null, 2)); 45 | }).fail(function() { 46 | resultEditor.getDoc().setValue("Failed to send request."); 47 | }).always(function() { 48 | $.unblockUI(); 49 | }); 50 | }); 51 | 52 | $exampleSelector.on("change", function() { 53 | var selectedScript = $(this).val(); 54 | if (selectedScript) { 55 | $.blockUI(); 56 | $.ajax({ 57 | url: selectedScript, 58 | mimeType: "text/x-groovy" 59 | }).done(function(data) { 60 | inputEditor.getDoc().setValue(data); 61 | }).fail(function() { 62 | resultEditor.getDoc().setValue("Failed to load example."); 63 | }).always(function() { 64 | $.unblockUI(); 65 | }); 66 | } 67 | }); 68 | 69 | $("#permalink-button").on("click", function() { 70 | var href = $location.attr('href').replace(/#.*/g, ''); 71 | resultEditor.getDoc().setValue(href + '#' + LZString.compressToEncodedURIComponent(inputEditor.getValue())); 72 | }); 73 | 74 | $window.on('hashchange', hashChangeEventHandler); 75 | $window.trigger('hashchange'); 76 | })(); 77 | -------------------------------------------------------------------------------- /src/main/resources/public/console/js/groovy.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: http://codemirror.net/LICENSE 3 | 4 | (function(mod) { 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS 6 | mod(require("../../lib/codemirror")); 7 | else if (typeof define == "function" && define.amd) // AMD 8 | define(["../../lib/codemirror"], mod); 9 | else // Plain browser env 10 | mod(CodeMirror); 11 | })(function(CodeMirror) { 12 | "use strict"; 13 | 14 | CodeMirror.defineMode("groovy", function(config) { 15 | function words(str) { 16 | var obj = {}, words = str.split(" "); 17 | for (var i = 0; i < words.length; ++i) obj[words[i]] = true; 18 | return obj; 19 | } 20 | var keywords = words( 21 | "abstract as assert boolean break byte case catch char class const continue def default " + 22 | "do double else enum extends final finally float for goto if implements import in " + 23 | "instanceof int interface long native new package private protected public return " + 24 | "short static strictfp super switch synchronized threadsafe throw throws trait transient " + 25 | "try void volatile while"); 26 | var blockKeywords = words("catch class def do else enum finally for if interface switch trait try while"); 27 | var standaloneKeywords = words("return break continue"); 28 | var atoms = words("null true false this"); 29 | 30 | var curPunc; 31 | function tokenBase(stream, state) { 32 | var ch = stream.next(); 33 | if (ch == '"' || ch == "'") { 34 | return startString(ch, stream, state); 35 | } 36 | if (/[\[\]{}\(\),;\:\.]/.test(ch)) { 37 | curPunc = ch; 38 | return null; 39 | } 40 | if (/\d/.test(ch)) { 41 | stream.eatWhile(/[\w\.]/); 42 | if (stream.eat(/eE/)) { stream.eat(/\+\-/); stream.eatWhile(/\d/); } 43 | return "number"; 44 | } 45 | if (ch == "/") { 46 | if (stream.eat("*")) { 47 | state.tokenize.push(tokenComment); 48 | return tokenComment(stream, state); 49 | } 50 | if (stream.eat("/")) { 51 | stream.skipToEnd(); 52 | return "comment"; 53 | } 54 | if (expectExpression(state.lastToken, false)) { 55 | return startString(ch, stream, state); 56 | } 57 | } 58 | if (ch == "-" && stream.eat(">")) { 59 | curPunc = "->"; 60 | return null; 61 | } 62 | if (/[+\-*&%=<>!?|\/~]/.test(ch)) { 63 | stream.eatWhile(/[+\-*&%=<>|~]/); 64 | return "operator"; 65 | } 66 | stream.eatWhile(/[\w\$_]/); 67 | if (ch == "@") { stream.eatWhile(/[\w\$_\.]/); return "meta"; } 68 | if (state.lastToken == ".") return "property"; 69 | if (stream.eat(":")) { curPunc = "proplabel"; return "property"; } 70 | var cur = stream.current(); 71 | if (atoms.propertyIsEnumerable(cur)) { return "atom"; } 72 | if (keywords.propertyIsEnumerable(cur)) { 73 | if (blockKeywords.propertyIsEnumerable(cur)) curPunc = "newstatement"; 74 | else if (standaloneKeywords.propertyIsEnumerable(cur)) curPunc = "standalone"; 75 | return "keyword"; 76 | } 77 | return "variable"; 78 | } 79 | tokenBase.isBase = true; 80 | 81 | function startString(quote, stream, state) { 82 | var tripleQuoted = false; 83 | if (quote != "/" && stream.eat(quote)) { 84 | if (stream.eat(quote)) tripleQuoted = true; 85 | else return "string"; 86 | } 87 | function t(stream, state) { 88 | var escaped = false, next, end = !tripleQuoted; 89 | while ((next = stream.next()) != null) { 90 | if (next == quote && !escaped) { 91 | if (!tripleQuoted) { break; } 92 | if (stream.match(quote + quote)) { end = true; break; } 93 | } 94 | if (quote == '"' && next == "$" && !escaped && stream.eat("{")) { 95 | state.tokenize.push(tokenBaseUntilBrace()); 96 | return "string"; 97 | } 98 | escaped = !escaped && next == "\\"; 99 | } 100 | if (end) state.tokenize.pop(); 101 | return "string"; 102 | } 103 | state.tokenize.push(t); 104 | return t(stream, state); 105 | } 106 | 107 | function tokenBaseUntilBrace() { 108 | var depth = 1; 109 | function t(stream, state) { 110 | if (stream.peek() == "}") { 111 | depth--; 112 | if (depth == 0) { 113 | state.tokenize.pop(); 114 | return state.tokenize[state.tokenize.length-1](stream, state); 115 | } 116 | } else if (stream.peek() == "{") { 117 | depth++; 118 | } 119 | return tokenBase(stream, state); 120 | } 121 | t.isBase = true; 122 | return t; 123 | } 124 | 125 | function tokenComment(stream, state) { 126 | var maybeEnd = false, ch; 127 | while (ch = stream.next()) { 128 | if (ch == "/" && maybeEnd) { 129 | state.tokenize.pop(); 130 | break; 131 | } 132 | maybeEnd = (ch == "*"); 133 | } 134 | return "comment"; 135 | } 136 | 137 | function expectExpression(last, newline) { 138 | return !last || last == "operator" || last == "->" || /[\.\[\{\(,;:]/.test(last) || 139 | last == "newstatement" || last == "keyword" || last == "proplabel" || 140 | (last == "standalone" && !newline); 141 | } 142 | 143 | function Context(indented, column, type, align, prev) { 144 | this.indented = indented; 145 | this.column = column; 146 | this.type = type; 147 | this.align = align; 148 | this.prev = prev; 149 | } 150 | function pushContext(state, col, type) { 151 | return state.context = new Context(state.indented, col, type, null, state.context); 152 | } 153 | function popContext(state) { 154 | var t = state.context.type; 155 | if (t == ")" || t == "]" || t == "}") 156 | state.indented = state.context.indented; 157 | return state.context = state.context.prev; 158 | } 159 | 160 | // Interface 161 | 162 | return { 163 | startState: function(basecolumn) { 164 | return { 165 | tokenize: [tokenBase], 166 | context: new Context((basecolumn || 0) - config.indentUnit, 0, "top", false), 167 | indented: 0, 168 | startOfLine: true, 169 | lastToken: null 170 | }; 171 | }, 172 | 173 | token: function(stream, state) { 174 | var ctx = state.context; 175 | if (stream.sol()) { 176 | if (ctx.align == null) ctx.align = false; 177 | state.indented = stream.indentation(); 178 | state.startOfLine = true; 179 | // Automatic semicolon insertion 180 | if (ctx.type == "statement" && !expectExpression(state.lastToken, true)) { 181 | popContext(state); ctx = state.context; 182 | } 183 | } 184 | if (stream.eatSpace()) return null; 185 | curPunc = null; 186 | var style = state.tokenize[state.tokenize.length-1](stream, state); 187 | if (style == "comment") return style; 188 | if (ctx.align == null) ctx.align = true; 189 | 190 | if ((curPunc == ";" || curPunc == ":") && ctx.type == "statement") popContext(state); 191 | // Handle indentation for {x -> \n ... } 192 | else if (curPunc == "->" && ctx.type == "statement" && ctx.prev.type == "}") { 193 | popContext(state); 194 | state.context.align = false; 195 | } 196 | else if (curPunc == "{") pushContext(state, stream.column(), "}"); 197 | else if (curPunc == "[") pushContext(state, stream.column(), "]"); 198 | else if (curPunc == "(") pushContext(state, stream.column(), ")"); 199 | else if (curPunc == "}") { 200 | while (ctx.type == "statement") ctx = popContext(state); 201 | if (ctx.type == "}") ctx = popContext(state); 202 | while (ctx.type == "statement") ctx = popContext(state); 203 | } 204 | else if (curPunc == ctx.type) popContext(state); 205 | else if (ctx.type == "}" || ctx.type == "top" || (ctx.type == "statement" && curPunc == "newstatement")) 206 | pushContext(state, stream.column(), "statement"); 207 | state.startOfLine = false; 208 | state.lastToken = curPunc || style; 209 | return style; 210 | }, 211 | 212 | indent: function(state, textAfter) { 213 | if (!state.tokenize[state.tokenize.length-1].isBase) return CodeMirror.Pass; 214 | var firstChar = textAfter && textAfter.charAt(0), ctx = state.context; 215 | if (ctx.type == "statement" && !expectExpression(state.lastToken, true)) ctx = ctx.prev; 216 | var closing = firstChar == ctx.type; 217 | if (ctx.type == "statement") return ctx.indented + (firstChar == "{" ? 0 : config.indentUnit); 218 | else if (ctx.align) return ctx.column + (closing ? 0 : 1); 219 | else return ctx.indented + (closing ? 0 : config.indentUnit); 220 | }, 221 | 222 | electricChars: "{}", 223 | closeBrackets: {triples: "'\""}, 224 | fold: "brace" 225 | }; 226 | }); 227 | 228 | CodeMirror.defineMIME("text/x-groovy", "groovy"); 229 | 230 | }); 231 | -------------------------------------------------------------------------------- /src/main/resources/public/console/js/javascript.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: http://codemirror.net/LICENSE 3 | 4 | (function(mod) { 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS 6 | mod(require("../../lib/codemirror")); 7 | else if (typeof define == "function" && define.amd) // AMD 8 | define(["../../lib/codemirror"], mod); 9 | else // Plain browser env 10 | mod(CodeMirror); 11 | })(function(CodeMirror) { 12 | "use strict"; 13 | 14 | CodeMirror.defineMode("javascript", function(config, parserConfig) { 15 | var indentUnit = config.indentUnit; 16 | var statementIndent = parserConfig.statementIndent; 17 | var jsonldMode = parserConfig.jsonld; 18 | var jsonMode = parserConfig.json || jsonldMode; 19 | var isTS = parserConfig.typescript; 20 | var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/; 21 | 22 | // Tokenizer 23 | 24 | var keywords = function(){ 25 | function kw(type) {return {type: type, style: "keyword"};} 26 | var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"), D = kw("keyword d"); 27 | var operator = kw("operator"), atom = {type: "atom", style: "atom"}; 28 | 29 | return { 30 | "if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B, 31 | "return": D, "break": D, "continue": D, "new": kw("new"), "delete": C, "void": C, "throw": C, 32 | "debugger": kw("debugger"), "var": kw("var"), "const": kw("var"), "let": kw("var"), 33 | "function": kw("function"), "catch": kw("catch"), 34 | "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"), 35 | "in": operator, "typeof": operator, "instanceof": operator, 36 | "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom, 37 | "this": kw("this"), "class": kw("class"), "super": kw("atom"), 38 | "yield": C, "export": kw("export"), "import": kw("import"), "extends": C, 39 | "await": C 40 | }; 41 | }(); 42 | 43 | var isOperatorChar = /[+\-*&%=<>!?|~^@]/; 44 | var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/; 45 | 46 | function readRegexp(stream) { 47 | var escaped = false, next, inSet = false; 48 | while ((next = stream.next()) != null) { 49 | if (!escaped) { 50 | if (next == "/" && !inSet) return; 51 | if (next == "[") inSet = true; 52 | else if (inSet && next == "]") inSet = false; 53 | } 54 | escaped = !escaped && next == "\\"; 55 | } 56 | } 57 | 58 | // Used as scratch variables to communicate multiple values without 59 | // consing up tons of objects. 60 | var type, content; 61 | function ret(tp, style, cont) { 62 | type = tp; content = cont; 63 | return style; 64 | } 65 | function tokenBase(stream, state) { 66 | var ch = stream.next(); 67 | if (ch == '"' || ch == "'") { 68 | state.tokenize = tokenString(ch); 69 | return state.tokenize(stream, state); 70 | } else if (ch == "." && stream.match(/^\d+(?:[eE][+\-]?\d+)?/)) { 71 | return ret("number", "number"); 72 | } else if (ch == "." && stream.match("..")) { 73 | return ret("spread", "meta"); 74 | } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) { 75 | return ret(ch); 76 | } else if (ch == "=" && stream.eat(">")) { 77 | return ret("=>", "operator"); 78 | } else if (ch == "0" && stream.eat(/x/i)) { 79 | stream.eatWhile(/[\da-f]/i); 80 | return ret("number", "number"); 81 | } else if (ch == "0" && stream.eat(/o/i)) { 82 | stream.eatWhile(/[0-7]/i); 83 | return ret("number", "number"); 84 | } else if (ch == "0" && stream.eat(/b/i)) { 85 | stream.eatWhile(/[01]/i); 86 | return ret("number", "number"); 87 | } else if (/\d/.test(ch)) { 88 | stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/); 89 | return ret("number", "number"); 90 | } else if (ch == "/") { 91 | if (stream.eat("*")) { 92 | state.tokenize = tokenComment; 93 | return tokenComment(stream, state); 94 | } else if (stream.eat("/")) { 95 | stream.skipToEnd(); 96 | return ret("comment", "comment"); 97 | } else if (expressionAllowed(stream, state, 1)) { 98 | readRegexp(stream); 99 | stream.match(/^\b(([gimyu])(?![gimyu]*\2))+\b/); 100 | return ret("regexp", "string-2"); 101 | } else { 102 | stream.eat("="); 103 | return ret("operator", "operator", stream.current()); 104 | } 105 | } else if (ch == "`") { 106 | state.tokenize = tokenQuasi; 107 | return tokenQuasi(stream, state); 108 | } else if (ch == "#") { 109 | stream.skipToEnd(); 110 | return ret("error", "error"); 111 | } else if (isOperatorChar.test(ch)) { 112 | if (ch != ">" || !state.lexical || state.lexical.type != ">") { 113 | if (stream.eat("=")) { 114 | if (ch == "!" || ch == "=") stream.eat("=") 115 | } else if (/[<>*+\-]/.test(ch)) { 116 | stream.eat(ch) 117 | if (ch == ">") stream.eat(ch) 118 | } 119 | } 120 | return ret("operator", "operator", stream.current()); 121 | } else if (wordRE.test(ch)) { 122 | stream.eatWhile(wordRE); 123 | var word = stream.current() 124 | if (state.lastType != ".") { 125 | if (keywords.propertyIsEnumerable(word)) { 126 | var kw = keywords[word] 127 | return ret(kw.type, kw.style, word) 128 | } 129 | if (word == "async" && stream.match(/^(\s|\/\*.*?\*\/)*[\(\w]/, false)) 130 | return ret("async", "keyword", word) 131 | } 132 | return ret("variable", "variable", word) 133 | } 134 | } 135 | 136 | function tokenString(quote) { 137 | return function(stream, state) { 138 | var escaped = false, next; 139 | if (jsonldMode && stream.peek() == "@" && stream.match(isJsonldKeyword)){ 140 | state.tokenize = tokenBase; 141 | return ret("jsonld-keyword", "meta"); 142 | } 143 | while ((next = stream.next()) != null) { 144 | if (next == quote && !escaped) break; 145 | escaped = !escaped && next == "\\"; 146 | } 147 | if (!escaped) state.tokenize = tokenBase; 148 | return ret("string", "string"); 149 | }; 150 | } 151 | 152 | function tokenComment(stream, state) { 153 | var maybeEnd = false, ch; 154 | while (ch = stream.next()) { 155 | if (ch == "/" && maybeEnd) { 156 | state.tokenize = tokenBase; 157 | break; 158 | } 159 | maybeEnd = (ch == "*"); 160 | } 161 | return ret("comment", "comment"); 162 | } 163 | 164 | function tokenQuasi(stream, state) { 165 | var escaped = false, next; 166 | while ((next = stream.next()) != null) { 167 | if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) { 168 | state.tokenize = tokenBase; 169 | break; 170 | } 171 | escaped = !escaped && next == "\\"; 172 | } 173 | return ret("quasi", "string-2", stream.current()); 174 | } 175 | 176 | var brackets = "([{}])"; 177 | // This is a crude lookahead trick to try and notice that we're 178 | // parsing the argument patterns for a fat-arrow function before we 179 | // actually hit the arrow token. It only works if the arrow is on 180 | // the same line as the arguments and there's no strange noise 181 | // (comments) in between. Fallback is to only notice when we hit the 182 | // arrow, and not declare the arguments as locals for the arrow 183 | // body. 184 | function findFatArrow(stream, state) { 185 | if (state.fatArrowAt) state.fatArrowAt = null; 186 | var arrow = stream.string.indexOf("=>", stream.start); 187 | if (arrow < 0) return; 188 | 189 | if (isTS) { // Try to skip TypeScript return type declarations after the arguments 190 | var m = /:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(stream.string.slice(stream.start, arrow)) 191 | if (m) arrow = m.index 192 | } 193 | 194 | var depth = 0, sawSomething = false; 195 | for (var pos = arrow - 1; pos >= 0; --pos) { 196 | var ch = stream.string.charAt(pos); 197 | var bracket = brackets.indexOf(ch); 198 | if (bracket >= 0 && bracket < 3) { 199 | if (!depth) { ++pos; break; } 200 | if (--depth == 0) { if (ch == "(") sawSomething = true; break; } 201 | } else if (bracket >= 3 && bracket < 6) { 202 | ++depth; 203 | } else if (wordRE.test(ch)) { 204 | sawSomething = true; 205 | } else if (/["'\/]/.test(ch)) { 206 | return; 207 | } else if (sawSomething && !depth) { 208 | ++pos; 209 | break; 210 | } 211 | } 212 | if (sawSomething && !depth) state.fatArrowAt = pos; 213 | } 214 | 215 | // Parser 216 | 217 | var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true, "jsonld-keyword": true}; 218 | 219 | function JSLexical(indented, column, type, align, prev, info) { 220 | this.indented = indented; 221 | this.column = column; 222 | this.type = type; 223 | this.prev = prev; 224 | this.info = info; 225 | if (align != null) this.align = align; 226 | } 227 | 228 | function inScope(state, varname) { 229 | for (var v = state.localVars; v; v = v.next) 230 | if (v.name == varname) return true; 231 | for (var cx = state.context; cx; cx = cx.prev) { 232 | for (var v = cx.vars; v; v = v.next) 233 | if (v.name == varname) return true; 234 | } 235 | } 236 | 237 | function parseJS(state, style, type, content, stream) { 238 | var cc = state.cc; 239 | // Communicate our context to the combinators. 240 | // (Less wasteful than consing up a hundred closures on every call.) 241 | cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style; 242 | 243 | if (!state.lexical.hasOwnProperty("align")) 244 | state.lexical.align = true; 245 | 246 | while(true) { 247 | var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement; 248 | if (combinator(type, content)) { 249 | while(cc.length && cc[cc.length - 1].lex) 250 | cc.pop()(); 251 | if (cx.marked) return cx.marked; 252 | if (type == "variable" && inScope(state, content)) return "variable-2"; 253 | return style; 254 | } 255 | } 256 | } 257 | 258 | // Combinator utils 259 | 260 | var cx = {state: null, column: null, marked: null, cc: null}; 261 | function pass() { 262 | for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]); 263 | } 264 | function cont() { 265 | pass.apply(null, arguments); 266 | return true; 267 | } 268 | function register(varname) { 269 | function inList(list) { 270 | for (var v = list; v; v = v.next) 271 | if (v.name == varname) return true; 272 | return false; 273 | } 274 | var state = cx.state; 275 | cx.marked = "def"; 276 | if (state.context) { 277 | if (inList(state.localVars)) return; 278 | state.localVars = {name: varname, next: state.localVars}; 279 | } else { 280 | if (inList(state.globalVars)) return; 281 | if (parserConfig.globalVars) 282 | state.globalVars = {name: varname, next: state.globalVars}; 283 | } 284 | } 285 | 286 | function isModifier(name) { 287 | return name == "public" || name == "private" || name == "protected" || name == "abstract" || name == "readonly" 288 | } 289 | 290 | // Combinators 291 | 292 | var defaultVars = {name: "this", next: {name: "arguments"}}; 293 | function pushcontext() { 294 | cx.state.context = {prev: cx.state.context, vars: cx.state.localVars}; 295 | cx.state.localVars = defaultVars; 296 | } 297 | function popcontext() { 298 | cx.state.localVars = cx.state.context.vars; 299 | cx.state.context = cx.state.context.prev; 300 | } 301 | function pushlex(type, info) { 302 | var result = function() { 303 | var state = cx.state, indent = state.indented; 304 | if (state.lexical.type == "stat") indent = state.lexical.indented; 305 | else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev) 306 | indent = outer.indented; 307 | state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info); 308 | }; 309 | result.lex = true; 310 | return result; 311 | } 312 | function poplex() { 313 | var state = cx.state; 314 | if (state.lexical.prev) { 315 | if (state.lexical.type == ")") 316 | state.indented = state.lexical.indented; 317 | state.lexical = state.lexical.prev; 318 | } 319 | } 320 | poplex.lex = true; 321 | 322 | function expect(wanted) { 323 | function exp(type) { 324 | if (type == wanted) return cont(); 325 | else if (wanted == ";") return pass(); 326 | else return cont(exp); 327 | }; 328 | return exp; 329 | } 330 | 331 | function statement(type, value) { 332 | if (type == "var") return cont(pushlex("vardef", value.length), vardef, expect(";"), poplex); 333 | if (type == "keyword a") return cont(pushlex("form"), parenExpr, statement, poplex); 334 | if (type == "keyword b") return cont(pushlex("form"), statement, poplex); 335 | if (type == "keyword d") return cx.stream.match(/^\s*$/, false) ? cont() : cont(pushlex("stat"), maybeexpression, expect(";"), poplex); 336 | if (type == "debugger") return cont(expect(";")); 337 | if (type == "{") return cont(pushlex("}"), block, poplex); 338 | if (type == ";") return cont(); 339 | if (type == "if") { 340 | if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex) 341 | cx.state.cc.pop()(); 342 | return cont(pushlex("form"), parenExpr, statement, poplex, maybeelse); 343 | } 344 | if (type == "function") return cont(functiondef); 345 | if (type == "for") return cont(pushlex("form"), forspec, statement, poplex); 346 | if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword"; return cont(pushlex("form"), className, poplex); } 347 | if (type == "variable") { 348 | if (isTS && value == "type") { 349 | cx.marked = "keyword" 350 | return cont(typeexpr, expect("operator"), typeexpr, expect(";")); 351 | } else if (isTS && value == "declare") { 352 | cx.marked = "keyword" 353 | return cont(statement) 354 | } else if (isTS && (value == "module" || value == "enum") && cx.stream.match(/^\s*\w/, false)) { 355 | cx.marked = "keyword" 356 | return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex) 357 | } else if (isTS && value == "namespace") { 358 | cx.marked = "keyword" 359 | return cont(pushlex("form"), expression, block, poplex) 360 | } else { 361 | return cont(pushlex("stat"), maybelabel); 362 | } 363 | } 364 | if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"), 365 | block, poplex, poplex); 366 | if (type == "case") return cont(expression, expect(":")); 367 | if (type == "default") return cont(expect(":")); 368 | if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"), 369 | statement, poplex, popcontext); 370 | if (type == "export") return cont(pushlex("stat"), afterExport, poplex); 371 | if (type == "import") return cont(pushlex("stat"), afterImport, poplex); 372 | if (type == "async") return cont(statement) 373 | if (value == "@") return cont(expression, statement) 374 | return pass(pushlex("stat"), expression, expect(";"), poplex); 375 | } 376 | function expression(type, value) { 377 | return expressionInner(type, value, false); 378 | } 379 | function expressionNoComma(type, value) { 380 | return expressionInner(type, value, true); 381 | } 382 | function parenExpr(type) { 383 | if (type != "(") return pass() 384 | return cont(pushlex(")"), expression, expect(")"), poplex) 385 | } 386 | function expressionInner(type, value, noComma) { 387 | if (cx.state.fatArrowAt == cx.stream.start) { 388 | var body = noComma ? arrowBodyNoComma : arrowBody; 389 | if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, expect("=>"), body, popcontext); 390 | else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext); 391 | } 392 | 393 | var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma; 394 | if (atomicTypes.hasOwnProperty(type)) return cont(maybeop); 395 | if (type == "function") return cont(functiondef, maybeop); 396 | if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword"; return cont(pushlex("form"), classExpression, poplex); } 397 | if (type == "keyword c" || type == "async") return cont(noComma ? expressionNoComma : expression); 398 | if (type == "(") return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeop); 399 | if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression); 400 | if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop); 401 | if (type == "{") return contCommasep(objprop, "}", null, maybeop); 402 | if (type == "quasi") return pass(quasi, maybeop); 403 | if (type == "new") return cont(maybeTarget(noComma)); 404 | return cont(); 405 | } 406 | function maybeexpression(type) { 407 | if (type.match(/[;\}\)\],]/)) return pass(); 408 | return pass(expression); 409 | } 410 | 411 | function maybeoperatorComma(type, value) { 412 | if (type == ",") return cont(expression); 413 | return maybeoperatorNoComma(type, value, false); 414 | } 415 | function maybeoperatorNoComma(type, value, noComma) { 416 | var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma; 417 | var expr = noComma == false ? expression : expressionNoComma; 418 | if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext); 419 | if (type == "operator") { 420 | if (/\+\+|--/.test(value) || isTS && value == "!") return cont(me); 421 | if (isTS && value == "<" && cx.stream.match(/^([^>]|<.*?>)*>\s*\(/, false)) 422 | return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, me); 423 | if (value == "?") return cont(expression, expect(":"), expr); 424 | return cont(expr); 425 | } 426 | if (type == "quasi") { return pass(quasi, me); } 427 | if (type == ";") return; 428 | if (type == "(") return contCommasep(expressionNoComma, ")", "call", me); 429 | if (type == ".") return cont(property, me); 430 | if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me); 431 | if (isTS && value == "as") { cx.marked = "keyword"; return cont(typeexpr, me) } 432 | if (type == "regexp") { 433 | cx.state.lastType = cx.marked = "operator" 434 | cx.stream.backUp(cx.stream.pos - cx.stream.start - 1) 435 | return cont(expr) 436 | } 437 | } 438 | function quasi(type, value) { 439 | if (type != "quasi") return pass(); 440 | if (value.slice(value.length - 2) != "${") return cont(quasi); 441 | return cont(expression, continueQuasi); 442 | } 443 | function continueQuasi(type) { 444 | if (type == "}") { 445 | cx.marked = "string-2"; 446 | cx.state.tokenize = tokenQuasi; 447 | return cont(quasi); 448 | } 449 | } 450 | function arrowBody(type) { 451 | findFatArrow(cx.stream, cx.state); 452 | return pass(type == "{" ? statement : expression); 453 | } 454 | function arrowBodyNoComma(type) { 455 | findFatArrow(cx.stream, cx.state); 456 | return pass(type == "{" ? statement : expressionNoComma); 457 | } 458 | function maybeTarget(noComma) { 459 | return function(type) { 460 | if (type == ".") return cont(noComma ? targetNoComma : target); 461 | else if (type == "variable" && isTS) return cont(maybeTypeArgs, noComma ? maybeoperatorNoComma : maybeoperatorComma) 462 | else return pass(noComma ? expressionNoComma : expression); 463 | }; 464 | } 465 | function target(_, value) { 466 | if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorComma); } 467 | } 468 | function targetNoComma(_, value) { 469 | if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorNoComma); } 470 | } 471 | function maybelabel(type) { 472 | if (type == ":") return cont(poplex, statement); 473 | return pass(maybeoperatorComma, expect(";"), poplex); 474 | } 475 | function property(type) { 476 | if (type == "variable") {cx.marked = "property"; return cont();} 477 | } 478 | function objprop(type, value) { 479 | if (type == "async") { 480 | cx.marked = "property"; 481 | return cont(objprop); 482 | } else if (type == "variable" || cx.style == "keyword") { 483 | cx.marked = "property"; 484 | if (value == "get" || value == "set") return cont(getterSetter); 485 | var m // Work around fat-arrow-detection complication for detecting typescript typed arrow params 486 | if (isTS && cx.state.fatArrowAt == cx.stream.start && (m = cx.stream.match(/^\s*:\s*/, false))) 487 | cx.state.fatArrowAt = cx.stream.pos + m[0].length 488 | return cont(afterprop); 489 | } else if (type == "number" || type == "string") { 490 | cx.marked = jsonldMode ? "property" : (cx.style + " property"); 491 | return cont(afterprop); 492 | } else if (type == "jsonld-keyword") { 493 | return cont(afterprop); 494 | } else if (isTS && isModifier(value)) { 495 | cx.marked = "keyword" 496 | return cont(objprop) 497 | } else if (type == "[") { 498 | return cont(expression, maybetype, expect("]"), afterprop); 499 | } else if (type == "spread") { 500 | return cont(expressionNoComma, afterprop); 501 | } else if (value == "*") { 502 | cx.marked = "keyword"; 503 | return cont(objprop); 504 | } else if (type == ":") { 505 | return pass(afterprop) 506 | } 507 | } 508 | function getterSetter(type) { 509 | if (type != "variable") return pass(afterprop); 510 | cx.marked = "property"; 511 | return cont(functiondef); 512 | } 513 | function afterprop(type) { 514 | if (type == ":") return cont(expressionNoComma); 515 | if (type == "(") return pass(functiondef); 516 | } 517 | function commasep(what, end, sep) { 518 | function proceed(type, value) { 519 | if (sep ? sep.indexOf(type) > -1 : type == ",") { 520 | var lex = cx.state.lexical; 521 | if (lex.info == "call") lex.pos = (lex.pos || 0) + 1; 522 | return cont(function(type, value) { 523 | if (type == end || value == end) return pass() 524 | return pass(what) 525 | }, proceed); 526 | } 527 | if (type == end || value == end) return cont(); 528 | return cont(expect(end)); 529 | } 530 | return function(type, value) { 531 | if (type == end || value == end) return cont(); 532 | return pass(what, proceed); 533 | }; 534 | } 535 | function contCommasep(what, end, info) { 536 | for (var i = 3; i < arguments.length; i++) 537 | cx.cc.push(arguments[i]); 538 | return cont(pushlex(end, info), commasep(what, end), poplex); 539 | } 540 | function block(type) { 541 | if (type == "}") return cont(); 542 | return pass(statement, block); 543 | } 544 | function maybetype(type, value) { 545 | if (isTS) { 546 | if (type == ":") return cont(typeexpr); 547 | if (value == "?") return cont(maybetype); 548 | } 549 | } 550 | function mayberettype(type) { 551 | if (isTS && type == ":") { 552 | if (cx.stream.match(/^\s*\w+\s+is\b/, false)) return cont(expression, isKW, typeexpr) 553 | else return cont(typeexpr) 554 | } 555 | } 556 | function isKW(_, value) { 557 | if (value == "is") { 558 | cx.marked = "keyword" 559 | return cont() 560 | } 561 | } 562 | function typeexpr(type, value) { 563 | if (type == "variable" || value == "void") { 564 | if (value == "keyof") { 565 | cx.marked = "keyword" 566 | return cont(typeexpr) 567 | } else { 568 | cx.marked = "type" 569 | return cont(afterType) 570 | } 571 | } 572 | if (type == "string" || type == "number" || type == "atom") return cont(afterType); 573 | if (type == "[") return cont(pushlex("]"), commasep(typeexpr, "]", ","), poplex, afterType) 574 | if (type == "{") return cont(pushlex("}"), commasep(typeprop, "}", ",;"), poplex, afterType) 575 | if (type == "(") return cont(commasep(typearg, ")"), maybeReturnType) 576 | } 577 | function maybeReturnType(type) { 578 | if (type == "=>") return cont(typeexpr) 579 | } 580 | function typeprop(type, value) { 581 | if (type == "variable" || cx.style == "keyword") { 582 | cx.marked = "property" 583 | return cont(typeprop) 584 | } else if (value == "?") { 585 | return cont(typeprop) 586 | } else if (type == ":") { 587 | return cont(typeexpr) 588 | } else if (type == "[") { 589 | return cont(expression, maybetype, expect("]"), typeprop) 590 | } 591 | } 592 | function typearg(type) { 593 | if (type == "variable") return cont(typearg) 594 | else if (type == ":") return cont(typeexpr) 595 | } 596 | function afterType(type, value) { 597 | if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType) 598 | if (value == "|" || type == ".") return cont(typeexpr) 599 | if (type == "[") return cont(expect("]"), afterType) 600 | if (value == "extends" || value == "implements") { cx.marked = "keyword"; return cont(typeexpr) } 601 | } 602 | function maybeTypeArgs(_, value) { 603 | if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType) 604 | } 605 | function typeparam() { 606 | return pass(typeexpr, maybeTypeDefault) 607 | } 608 | function maybeTypeDefault(_, value) { 609 | if (value == "=") return cont(typeexpr) 610 | } 611 | function vardef() { 612 | return pass(pattern, maybetype, maybeAssign, vardefCont); 613 | } 614 | function pattern(type, value) { 615 | if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(pattern) } 616 | if (type == "variable") { register(value); return cont(); } 617 | if (type == "spread") return cont(pattern); 618 | if (type == "[") return contCommasep(pattern, "]"); 619 | if (type == "{") return contCommasep(proppattern, "}"); 620 | } 621 | function proppattern(type, value) { 622 | if (type == "variable" && !cx.stream.match(/^\s*:/, false)) { 623 | register(value); 624 | return cont(maybeAssign); 625 | } 626 | if (type == "variable") cx.marked = "property"; 627 | if (type == "spread") return cont(pattern); 628 | if (type == "}") return pass(); 629 | return cont(expect(":"), pattern, maybeAssign); 630 | } 631 | function maybeAssign(_type, value) { 632 | if (value == "=") return cont(expressionNoComma); 633 | } 634 | function vardefCont(type) { 635 | if (type == ",") return cont(vardef); 636 | } 637 | function maybeelse(type, value) { 638 | if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex); 639 | } 640 | function forspec(type) { 641 | if (type == "(") return cont(pushlex(")"), forspec1, expect(")"), poplex); 642 | } 643 | function forspec1(type) { 644 | if (type == "var") return cont(vardef, expect(";"), forspec2); 645 | if (type == ";") return cont(forspec2); 646 | if (type == "variable") return cont(formaybeinof); 647 | return pass(expression, expect(";"), forspec2); 648 | } 649 | function formaybeinof(_type, value) { 650 | if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); } 651 | return cont(maybeoperatorComma, forspec2); 652 | } 653 | function forspec2(type, value) { 654 | if (type == ";") return cont(forspec3); 655 | if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); } 656 | return pass(expression, expect(";"), forspec3); 657 | } 658 | function forspec3(type) { 659 | if (type != ")") cont(expression); 660 | } 661 | function functiondef(type, value) { 662 | if (value == "*") {cx.marked = "keyword"; return cont(functiondef);} 663 | if (type == "variable") {register(value); return cont(functiondef);} 664 | if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, statement, popcontext); 665 | if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondef) 666 | } 667 | function funarg(type, value) { 668 | if (value == "@") cont(expression, funarg) 669 | if (type == "spread") return cont(funarg); 670 | if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(funarg); } 671 | return pass(pattern, maybetype, maybeAssign); 672 | } 673 | function classExpression(type, value) { 674 | // Class expressions may have an optional name. 675 | if (type == "variable") return className(type, value); 676 | return classNameAfter(type, value); 677 | } 678 | function className(type, value) { 679 | if (type == "variable") {register(value); return cont(classNameAfter);} 680 | } 681 | function classNameAfter(type, value) { 682 | if (value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, classNameAfter) 683 | if (value == "extends" || value == "implements" || (isTS && type == ",")) 684 | return cont(isTS ? typeexpr : expression, classNameAfter); 685 | if (type == "{") return cont(pushlex("}"), classBody, poplex); 686 | } 687 | function classBody(type, value) { 688 | if (type == "async" || 689 | (type == "variable" && 690 | (value == "static" || value == "get" || value == "set" || (isTS && isModifier(value))) && 691 | cx.stream.match(/^\s+[\w$\xa1-\uffff]/, false))) { 692 | cx.marked = "keyword"; 693 | return cont(classBody); 694 | } 695 | if (type == "variable" || cx.style == "keyword") { 696 | cx.marked = "property"; 697 | return cont(isTS ? classfield : functiondef, classBody); 698 | } 699 | if (type == "[") 700 | return cont(expression, maybetype, expect("]"), isTS ? classfield : functiondef, classBody) 701 | if (value == "*") { 702 | cx.marked = "keyword"; 703 | return cont(classBody); 704 | } 705 | if (type == ";") return cont(classBody); 706 | if (type == "}") return cont(); 707 | if (value == "@") return cont(expression, classBody) 708 | } 709 | function classfield(type, value) { 710 | if (value == "?") return cont(classfield) 711 | if (type == ":") return cont(typeexpr, maybeAssign) 712 | if (value == "=") return cont(expressionNoComma) 713 | return pass(functiondef) 714 | } 715 | function afterExport(type, value) { 716 | if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); } 717 | if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); } 718 | if (type == "{") return cont(commasep(exportField, "}"), maybeFrom, expect(";")); 719 | return pass(statement); 720 | } 721 | function exportField(type, value) { 722 | if (value == "as") { cx.marked = "keyword"; return cont(expect("variable")); } 723 | if (type == "variable") return pass(expressionNoComma, exportField); 724 | } 725 | function afterImport(type) { 726 | if (type == "string") return cont(); 727 | return pass(importSpec, maybeMoreImports, maybeFrom); 728 | } 729 | function importSpec(type, value) { 730 | if (type == "{") return contCommasep(importSpec, "}"); 731 | if (type == "variable") register(value); 732 | if (value == "*") cx.marked = "keyword"; 733 | return cont(maybeAs); 734 | } 735 | function maybeMoreImports(type) { 736 | if (type == ",") return cont(importSpec, maybeMoreImports) 737 | } 738 | function maybeAs(_type, value) { 739 | if (value == "as") { cx.marked = "keyword"; return cont(importSpec); } 740 | } 741 | function maybeFrom(_type, value) { 742 | if (value == "from") { cx.marked = "keyword"; return cont(expression); } 743 | } 744 | function arrayLiteral(type) { 745 | if (type == "]") return cont(); 746 | return pass(commasep(expressionNoComma, "]")); 747 | } 748 | 749 | function isContinuedStatement(state, textAfter) { 750 | return state.lastType == "operator" || state.lastType == "," || 751 | isOperatorChar.test(textAfter.charAt(0)) || 752 | /[,.]/.test(textAfter.charAt(0)); 753 | } 754 | 755 | function expressionAllowed(stream, state, backUp) { 756 | return state.tokenize == tokenBase && 757 | /^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(state.lastType) || 758 | (state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0)))) 759 | } 760 | 761 | // Interface 762 | 763 | return { 764 | startState: function(basecolumn) { 765 | var state = { 766 | tokenize: tokenBase, 767 | lastType: "sof", 768 | cc: [], 769 | lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false), 770 | localVars: parserConfig.localVars, 771 | context: parserConfig.localVars && {vars: parserConfig.localVars}, 772 | indented: basecolumn || 0 773 | }; 774 | if (parserConfig.globalVars && typeof parserConfig.globalVars == "object") 775 | state.globalVars = parserConfig.globalVars; 776 | return state; 777 | }, 778 | 779 | token: function(stream, state) { 780 | if (stream.sol()) { 781 | if (!state.lexical.hasOwnProperty("align")) 782 | state.lexical.align = false; 783 | state.indented = stream.indentation(); 784 | findFatArrow(stream, state); 785 | } 786 | if (state.tokenize != tokenComment && stream.eatSpace()) return null; 787 | var style = state.tokenize(stream, state); 788 | if (type == "comment") return style; 789 | state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type; 790 | return parseJS(state, style, type, content, stream); 791 | }, 792 | 793 | indent: function(state, textAfter) { 794 | if (state.tokenize == tokenComment) return CodeMirror.Pass; 795 | if (state.tokenize != tokenBase) return 0; 796 | var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, top 797 | // Kludge to prevent 'maybelse' from blocking lexical scope pops 798 | if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) { 799 | var c = state.cc[i]; 800 | if (c == poplex) lexical = lexical.prev; 801 | else if (c != maybeelse) break; 802 | } 803 | while ((lexical.type == "stat" || lexical.type == "form") && 804 | (firstChar == "}" || ((top = state.cc[state.cc.length - 1]) && 805 | (top == maybeoperatorComma || top == maybeoperatorNoComma) && 806 | !/^[,\.=+\-*:?[\(]/.test(textAfter)))) 807 | lexical = lexical.prev; 808 | if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat") 809 | lexical = lexical.prev; 810 | var type = lexical.type, closing = firstChar == type; 811 | 812 | if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info + 1 : 0); 813 | else if (type == "form" && firstChar == "{") return lexical.indented; 814 | else if (type == "form") return lexical.indented + indentUnit; 815 | else if (type == "stat") 816 | return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0); 817 | else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false) 818 | return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit); 819 | else if (lexical.align) return lexical.column + (closing ? 0 : 1); 820 | else return lexical.indented + (closing ? 0 : indentUnit); 821 | }, 822 | 823 | electricInput: /^\s*(?:case .*?:|default:|\{|\})$/, 824 | blockCommentStart: jsonMode ? null : "/*", 825 | blockCommentEnd: jsonMode ? null : "*/", 826 | blockCommentContinue: jsonMode ? null : " * ", 827 | lineComment: jsonMode ? null : "//", 828 | fold: "brace", 829 | closeBrackets: "()[]{}''\"\"``", 830 | 831 | helperType: jsonMode ? "json" : "javascript", 832 | jsonldMode: jsonldMode, 833 | jsonMode: jsonMode, 834 | 835 | expressionAllowed: expressionAllowed, 836 | 837 | skipExpression: function(state) { 838 | var top = state.cc[state.cc.length - 1] 839 | if (top == expression || top == expressionNoComma) state.cc.pop() 840 | } 841 | }; 842 | }); 843 | 844 | CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/); 845 | 846 | CodeMirror.defineMIME("text/javascript", "javascript"); 847 | CodeMirror.defineMIME("text/ecmascript", "javascript"); 848 | CodeMirror.defineMIME("application/javascript", "javascript"); 849 | CodeMirror.defineMIME("application/x-javascript", "javascript"); 850 | CodeMirror.defineMIME("application/ecmascript", "javascript"); 851 | CodeMirror.defineMIME("application/json", {name: "javascript", json: true}); 852 | CodeMirror.defineMIME("application/x-json", {name: "javascript", json: true}); 853 | CodeMirror.defineMIME("application/ld+json", {name: "javascript", jsonld: true}); 854 | CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true }); 855 | CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true }); 856 | 857 | }); 858 | -------------------------------------------------------------------------------- /src/main/resources/public/console/js/jquery.blockui.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery blockUI plugin 3 | * Version 2.70.0-2014.11.23 4 | * Requires jQuery v1.7 or later 5 | * 6 | * Examples at: http://malsup.com/jquery/block/ 7 | * Copyright (c) 2007-2013 M. Alsup 8 | * Dual licensed under the MIT and GPL licenses: 9 | * http://www.opensource.org/licenses/mit-license.php 10 | * http://www.gnu.org/licenses/gpl.html 11 | * 12 | * Thanks to Amir-Hossein Sobhi for some excellent contributions! 13 | */ 14 | 15 | ;(function() { 16 | /*jshint eqeqeq:false curly:false latedef:false */ 17 | "use strict"; 18 | 19 | function setup($) { 20 | $.fn._fadeIn = $.fn.fadeIn; 21 | 22 | var noOp = $.noop || function() {}; 23 | 24 | // this bit is to ensure we don't call setExpression when we shouldn't (with extra muscle to handle 25 | // confusing userAgent strings on Vista) 26 | var msie = /MSIE/.test(navigator.userAgent); 27 | var ie6 = /MSIE 6.0/.test(navigator.userAgent) && ! /MSIE 8.0/.test(navigator.userAgent); 28 | var mode = document.documentMode || 0; 29 | var setExpr = $.isFunction( document.createElement('div').style.setExpression ); 30 | 31 | // global $ methods for blocking/unblocking the entire page 32 | $.blockUI = function(opts) { install(window, opts); }; 33 | $.unblockUI = function(opts) { remove(window, opts); }; 34 | 35 | // convenience method for quick growl-like notifications (http://www.google.com/search?q=growl) 36 | $.growlUI = function(title, message, timeout, onClose) { 37 | var $m = $('
'); 38 | if (title) $m.append('

'+title+'

'); 39 | if (message) $m.append('

'+message+'

'); 40 | if (timeout === undefined) timeout = 3000; 41 | 42 | // Added by konapun: Set timeout to 30 seconds if this growl is moused over, like normal toast notifications 43 | var callBlock = function(opts) { 44 | opts = opts || {}; 45 | 46 | $.blockUI({ 47 | message: $m, 48 | fadeIn : typeof opts.fadeIn !== 'undefined' ? opts.fadeIn : 700, 49 | fadeOut: typeof opts.fadeOut !== 'undefined' ? opts.fadeOut : 1000, 50 | timeout: typeof opts.timeout !== 'undefined' ? opts.timeout : timeout, 51 | centerY: false, 52 | showOverlay: false, 53 | onUnblock: onClose, 54 | css: $.blockUI.defaults.growlCSS 55 | }); 56 | }; 57 | 58 | callBlock(); 59 | var nonmousedOpacity = $m.css('opacity'); 60 | $m.mouseover(function() { 61 | callBlock({ 62 | fadeIn: 0, 63 | timeout: 30000 64 | }); 65 | 66 | var displayBlock = $('.blockMsg'); 67 | displayBlock.stop(); // cancel fadeout if it has started 68 | displayBlock.fadeTo(300, 1); // make it easier to read the message by removing transparency 69 | }).mouseout(function() { 70 | $('.blockMsg').fadeOut(1000); 71 | }); 72 | // End konapun additions 73 | }; 74 | 75 | // plugin method for blocking element content 76 | $.fn.block = function(opts) { 77 | if ( this[0] === window ) { 78 | $.blockUI( opts ); 79 | return this; 80 | } 81 | var fullOpts = $.extend({}, $.blockUI.defaults, opts || {}); 82 | this.each(function() { 83 | var $el = $(this); 84 | if (fullOpts.ignoreIfBlocked && $el.data('blockUI.isBlocked')) 85 | return; 86 | $el.unblock({ fadeOut: 0 }); 87 | }); 88 | 89 | return this.each(function() { 90 | if ($.css(this,'position') == 'static') { 91 | this.style.position = 'relative'; 92 | $(this).data('blockUI.static', true); 93 | } 94 | this.style.zoom = 1; // force 'hasLayout' in ie 95 | install(this, opts); 96 | }); 97 | }; 98 | 99 | // plugin method for unblocking element content 100 | $.fn.unblock = function(opts) { 101 | if ( this[0] === window ) { 102 | $.unblockUI( opts ); 103 | return this; 104 | } 105 | return this.each(function() { 106 | remove(this, opts); 107 | }); 108 | }; 109 | 110 | $.blockUI.version = 2.70; // 2nd generation blocking at no extra cost! 111 | 112 | // override these in your code to change the default behavior and style 113 | $.blockUI.defaults = { 114 | // message displayed when blocking (use null for no message) 115 | message: '

Please wait...

', 116 | 117 | title: null, // title string; only used when theme == true 118 | draggable: true, // only used when theme == true (requires jquery-ui.js to be loaded) 119 | 120 | theme: false, // set to true to use with jQuery UI themes 121 | 122 | // styles for the message when blocking; if you wish to disable 123 | // these and use an external stylesheet then do this in your code: 124 | // $.blockUI.defaults.css = {}; 125 | css: { 126 | padding: 0, 127 | margin: 0, 128 | width: '30%', 129 | top: '40%', 130 | left: '35%', 131 | textAlign: 'center', 132 | color: '#000', 133 | border: '3px solid #aaa', 134 | backgroundColor:'#fff', 135 | cursor: 'wait' 136 | }, 137 | 138 | // minimal style set used when themes are used 139 | themedCSS: { 140 | width: '30%', 141 | top: '40%', 142 | left: '35%' 143 | }, 144 | 145 | // styles for the overlay 146 | overlayCSS: { 147 | backgroundColor: '#000', 148 | opacity: 0.6, 149 | cursor: 'wait' 150 | }, 151 | 152 | // style to replace wait cursor before unblocking to correct issue 153 | // of lingering wait cursor 154 | cursorReset: 'default', 155 | 156 | // styles applied when using $.growlUI 157 | growlCSS: { 158 | width: '350px', 159 | top: '10px', 160 | left: '', 161 | right: '10px', 162 | border: 'none', 163 | padding: '5px', 164 | opacity: 0.6, 165 | cursor: 'default', 166 | color: '#fff', 167 | backgroundColor: '#000', 168 | '-webkit-border-radius':'10px', 169 | '-moz-border-radius': '10px', 170 | 'border-radius': '10px' 171 | }, 172 | 173 | // IE issues: 'about:blank' fails on HTTPS and javascript:false is s-l-o-w 174 | // (hat tip to Jorge H. N. de Vasconcelos) 175 | /*jshint scripturl:true */ 176 | iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank', 177 | 178 | // force usage of iframe in non-IE browsers (handy for blocking applets) 179 | forceIframe: false, 180 | 181 | // z-index for the blocking overlay 182 | baseZ: 1000, 183 | 184 | // set these to true to have the message automatically centered 185 | centerX: true, // <-- only effects element blocking (page block controlled via css above) 186 | centerY: true, 187 | 188 | // allow body element to be stetched in ie6; this makes blocking look better 189 | // on "short" pages. disable if you wish to prevent changes to the body height 190 | allowBodyStretch: true, 191 | 192 | // enable if you want key and mouse events to be disabled for content that is blocked 193 | bindEvents: true, 194 | 195 | // be default blockUI will supress tab navigation from leaving blocking content 196 | // (if bindEvents is true) 197 | constrainTabKey: true, 198 | 199 | // fadeIn time in millis; set to 0 to disable fadeIn on block 200 | fadeIn: 200, 201 | 202 | // fadeOut time in millis; set to 0 to disable fadeOut on unblock 203 | fadeOut: 400, 204 | 205 | // time in millis to wait before auto-unblocking; set to 0 to disable auto-unblock 206 | timeout: 0, 207 | 208 | // disable if you don't want to show the overlay 209 | showOverlay: true, 210 | 211 | // if true, focus will be placed in the first available input field when 212 | // page blocking 213 | focusInput: true, 214 | 215 | // elements that can receive focus 216 | focusableElements: ':input:enabled:visible', 217 | 218 | // suppresses the use of overlay styles on FF/Linux (due to performance issues with opacity) 219 | // no longer needed in 2012 220 | // applyPlatformOpacityRules: true, 221 | 222 | // callback method invoked when fadeIn has completed and blocking message is visible 223 | onBlock: null, 224 | 225 | // callback method invoked when unblocking has completed; the callback is 226 | // passed the element that has been unblocked (which is the window object for page 227 | // blocks) and the options that were passed to the unblock call: 228 | // onUnblock(element, options) 229 | onUnblock: null, 230 | 231 | // callback method invoked when the overlay area is clicked. 232 | // setting this will turn the cursor to a pointer, otherwise cursor defined in overlayCss will be used. 233 | onOverlayClick: null, 234 | 235 | // don't ask; if you really must know: http://groups.google.com/group/jquery-en/browse_thread/thread/36640a8730503595/2f6a79a77a78e493#2f6a79a77a78e493 236 | quirksmodeOffsetHack: 4, 237 | 238 | // class name of the message block 239 | blockMsgClass: 'blockMsg', 240 | 241 | // if it is already blocked, then ignore it (don't unblock and reblock) 242 | ignoreIfBlocked: false 243 | }; 244 | 245 | // private data and functions follow... 246 | 247 | var pageBlock = null; 248 | var pageBlockEls = []; 249 | 250 | function install(el, opts) { 251 | var css, themedCSS; 252 | var full = (el == window); 253 | var msg = (opts && opts.message !== undefined ? opts.message : undefined); 254 | opts = $.extend({}, $.blockUI.defaults, opts || {}); 255 | 256 | if (opts.ignoreIfBlocked && $(el).data('blockUI.isBlocked')) 257 | return; 258 | 259 | opts.overlayCSS = $.extend({}, $.blockUI.defaults.overlayCSS, opts.overlayCSS || {}); 260 | css = $.extend({}, $.blockUI.defaults.css, opts.css || {}); 261 | if (opts.onOverlayClick) 262 | opts.overlayCSS.cursor = 'pointer'; 263 | 264 | themedCSS = $.extend({}, $.blockUI.defaults.themedCSS, opts.themedCSS || {}); 265 | msg = msg === undefined ? opts.message : msg; 266 | 267 | // remove the current block (if there is one) 268 | if (full && pageBlock) 269 | remove(window, {fadeOut:0}); 270 | 271 | // if an existing element is being used as the blocking content then we capture 272 | // its current place in the DOM (and current display style) so we can restore 273 | // it when we unblock 274 | if (msg && typeof msg != 'string' && (msg.parentNode || msg.jquery)) { 275 | var node = msg.jquery ? msg[0] : msg; 276 | var data = {}; 277 | $(el).data('blockUI.history', data); 278 | data.el = node; 279 | data.parent = node.parentNode; 280 | data.display = node.style.display; 281 | data.position = node.style.position; 282 | if (data.parent) 283 | data.parent.removeChild(node); 284 | } 285 | 286 | $(el).data('blockUI.onUnblock', opts.onUnblock); 287 | var z = opts.baseZ; 288 | 289 | // blockUI uses 3 layers for blocking, for simplicity they are all used on every platform; 290 | // layer1 is the iframe layer which is used to supress bleed through of underlying content 291 | // layer2 is the overlay layer which has opacity and a wait cursor (by default) 292 | // layer3 is the message content that is displayed while blocking 293 | var lyr1, lyr2, lyr3, s; 294 | if (msie || opts.forceIframe) 295 | lyr1 = $(''); 296 | else 297 | lyr1 = $(''); 298 | 299 | if (opts.theme) 300 | lyr2 = $(''); 301 | else 302 | lyr2 = $(''); 303 | 304 | if (opts.theme && full) { 305 | s = ''; 311 | } 312 | else if (opts.theme) { 313 | s = ''; 319 | } 320 | else if (full) { 321 | s = ''; 322 | } 323 | else { 324 | s = ''; 325 | } 326 | lyr3 = $(s); 327 | 328 | // if we have a message, style it 329 | if (msg) { 330 | if (opts.theme) { 331 | lyr3.css(themedCSS); 332 | lyr3.addClass('ui-widget-content'); 333 | } 334 | else 335 | lyr3.css(css); 336 | } 337 | 338 | // style the overlay 339 | if (!opts.theme /*&& (!opts.applyPlatformOpacityRules)*/) 340 | lyr2.css(opts.overlayCSS); 341 | lyr2.css('position', full ? 'fixed' : 'absolute'); 342 | 343 | // make iframe layer transparent in IE 344 | if (msie || opts.forceIframe) 345 | lyr1.css('opacity',0.0); 346 | 347 | //$([lyr1[0],lyr2[0],lyr3[0]]).appendTo(full ? 'body' : el); 348 | var layers = [lyr1,lyr2,lyr3], $par = full ? $('body') : $(el); 349 | $.each(layers, function() { 350 | this.appendTo($par); 351 | }); 352 | 353 | if (opts.theme && opts.draggable && $.fn.draggable) { 354 | lyr3.draggable({ 355 | handle: '.ui-dialog-titlebar', 356 | cancel: 'li' 357 | }); 358 | } 359 | 360 | // ie7 must use absolute positioning in quirks mode and to account for activex issues (when scrolling) 361 | var expr = setExpr && (!$.support.boxModel || $('object,embed', full ? null : el).length > 0); 362 | if (ie6 || expr) { 363 | // give body 100% height 364 | if (full && opts.allowBodyStretch && $.support.boxModel) 365 | $('html,body').css('height','100%'); 366 | 367 | // fix ie6 issue when blocked element has a border width 368 | if ((ie6 || !$.support.boxModel) && !full) { 369 | var t = sz(el,'borderTopWidth'), l = sz(el,'borderLeftWidth'); 370 | var fixT = t ? '(0 - '+t+')' : 0; 371 | var fixL = l ? '(0 - '+l+')' : 0; 372 | } 373 | 374 | // simulate fixed position 375 | $.each(layers, function(i,o) { 376 | var s = o[0].style; 377 | s.position = 'absolute'; 378 | if (i < 2) { 379 | if (full) 380 | s.setExpression('height','Math.max(document.body.scrollHeight, document.body.offsetHeight) - (jQuery.support.boxModel?0:'+opts.quirksmodeOffsetHack+') + "px"'); 381 | else 382 | s.setExpression('height','this.parentNode.offsetHeight + "px"'); 383 | if (full) 384 | s.setExpression('width','jQuery.support.boxModel && document.documentElement.clientWidth || document.body.clientWidth + "px"'); 385 | else 386 | s.setExpression('width','this.parentNode.offsetWidth + "px"'); 387 | if (fixL) s.setExpression('left', fixL); 388 | if (fixT) s.setExpression('top', fixT); 389 | } 390 | else if (opts.centerY) { 391 | if (full) s.setExpression('top','(document.documentElement.clientHeight || document.body.clientHeight) / 2 - (this.offsetHeight / 2) + (blah = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "px"'); 392 | s.marginTop = 0; 393 | } 394 | else if (!opts.centerY && full) { 395 | var top = (opts.css && opts.css.top) ? parseInt(opts.css.top, 10) : 0; 396 | var expression = '((document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + '+top+') + "px"'; 397 | s.setExpression('top',expression); 398 | } 399 | }); 400 | } 401 | 402 | // show the message 403 | if (msg) { 404 | if (opts.theme) 405 | lyr3.find('.ui-widget-content').append(msg); 406 | else 407 | lyr3.append(msg); 408 | if (msg.jquery || msg.nodeType) 409 | $(msg).show(); 410 | } 411 | 412 | if ((msie || opts.forceIframe) && opts.showOverlay) 413 | lyr1.show(); // opacity is zero 414 | if (opts.fadeIn) { 415 | var cb = opts.onBlock ? opts.onBlock : noOp; 416 | var cb1 = (opts.showOverlay && !msg) ? cb : noOp; 417 | var cb2 = msg ? cb : noOp; 418 | if (opts.showOverlay) 419 | lyr2._fadeIn(opts.fadeIn, cb1); 420 | if (msg) 421 | lyr3._fadeIn(opts.fadeIn, cb2); 422 | } 423 | else { 424 | if (opts.showOverlay) 425 | lyr2.show(); 426 | if (msg) 427 | lyr3.show(); 428 | if (opts.onBlock) 429 | opts.onBlock.bind(lyr3)(); 430 | } 431 | 432 | // bind key and mouse events 433 | bind(1, el, opts); 434 | 435 | if (full) { 436 | pageBlock = lyr3[0]; 437 | pageBlockEls = $(opts.focusableElements,pageBlock); 438 | if (opts.focusInput) 439 | setTimeout(focus, 20); 440 | } 441 | else 442 | center(lyr3[0], opts.centerX, opts.centerY); 443 | 444 | if (opts.timeout) { 445 | // auto-unblock 446 | var to = setTimeout(function() { 447 | if (full) 448 | $.unblockUI(opts); 449 | else 450 | $(el).unblock(opts); 451 | }, opts.timeout); 452 | $(el).data('blockUI.timeout', to); 453 | } 454 | } 455 | 456 | // remove the block 457 | function remove(el, opts) { 458 | var count; 459 | var full = (el == window); 460 | var $el = $(el); 461 | var data = $el.data('blockUI.history'); 462 | var to = $el.data('blockUI.timeout'); 463 | if (to) { 464 | clearTimeout(to); 465 | $el.removeData('blockUI.timeout'); 466 | } 467 | opts = $.extend({}, $.blockUI.defaults, opts || {}); 468 | bind(0, el, opts); // unbind events 469 | 470 | if (opts.onUnblock === null) { 471 | opts.onUnblock = $el.data('blockUI.onUnblock'); 472 | $el.removeData('blockUI.onUnblock'); 473 | } 474 | 475 | var els; 476 | if (full) // crazy selector to handle odd field errors in ie6/7 477 | els = $('body').children().filter('.blockUI').add('body > .blockUI'); 478 | else 479 | els = $el.find('>.blockUI'); 480 | 481 | // fix cursor issue 482 | if ( opts.cursorReset ) { 483 | if ( els.length > 1 ) 484 | els[1].style.cursor = opts.cursorReset; 485 | if ( els.length > 2 ) 486 | els[2].style.cursor = opts.cursorReset; 487 | } 488 | 489 | if (full) 490 | pageBlock = pageBlockEls = null; 491 | 492 | if (opts.fadeOut) { 493 | count = els.length; 494 | els.stop().fadeOut(opts.fadeOut, function() { 495 | if ( --count === 0) 496 | reset(els,data,opts,el); 497 | }); 498 | } 499 | else 500 | reset(els, data, opts, el); 501 | } 502 | 503 | // move blocking element back into the DOM where it started 504 | function reset(els,data,opts,el) { 505 | var $el = $(el); 506 | if ( $el.data('blockUI.isBlocked') ) 507 | return; 508 | 509 | els.each(function(i,o) { 510 | // remove via DOM calls so we don't lose event handlers 511 | if (this.parentNode) 512 | this.parentNode.removeChild(this); 513 | }); 514 | 515 | if (data && data.el) { 516 | data.el.style.display = data.display; 517 | data.el.style.position = data.position; 518 | data.el.style.cursor = 'default'; // #59 519 | if (data.parent) 520 | data.parent.appendChild(data.el); 521 | $el.removeData('blockUI.history'); 522 | } 523 | 524 | if ($el.data('blockUI.static')) { 525 | $el.css('position', 'static'); // #22 526 | } 527 | 528 | if (typeof opts.onUnblock == 'function') 529 | opts.onUnblock(el,opts); 530 | 531 | // fix issue in Safari 6 where block artifacts remain until reflow 532 | var body = $(document.body), w = body.width(), cssW = body[0].style.width; 533 | body.width(w-1).width(w); 534 | body[0].style.width = cssW; 535 | } 536 | 537 | // bind/unbind the handler 538 | function bind(b, el, opts) { 539 | var full = el == window, $el = $(el); 540 | 541 | // don't bother unbinding if there is nothing to unbind 542 | if (!b && (full && !pageBlock || !full && !$el.data('blockUI.isBlocked'))) 543 | return; 544 | 545 | $el.data('blockUI.isBlocked', b); 546 | 547 | // don't bind events when overlay is not in use or if bindEvents is false 548 | if (!full || !opts.bindEvents || (b && !opts.showOverlay)) 549 | return; 550 | 551 | // bind anchors and inputs for mouse and key events 552 | var events = 'mousedown mouseup keydown keypress keyup touchstart touchend touchmove'; 553 | if (b) 554 | $(document).bind(events, opts, handler); 555 | else 556 | $(document).unbind(events, handler); 557 | 558 | // former impl... 559 | // var $e = $('a,:input'); 560 | // b ? $e.bind(events, opts, handler) : $e.unbind(events, handler); 561 | } 562 | 563 | // event handler to suppress keyboard/mouse events when blocking 564 | function handler(e) { 565 | // allow tab navigation (conditionally) 566 | if (e.type === 'keydown' && e.keyCode && e.keyCode == 9) { 567 | if (pageBlock && e.data.constrainTabKey) { 568 | var els = pageBlockEls; 569 | var fwd = !e.shiftKey && e.target === els[els.length-1]; 570 | var back = e.shiftKey && e.target === els[0]; 571 | if (fwd || back) { 572 | setTimeout(function(){focus(back);},10); 573 | return false; 574 | } 575 | } 576 | } 577 | var opts = e.data; 578 | var target = $(e.target); 579 | if (target.hasClass('blockOverlay') && opts.onOverlayClick) 580 | opts.onOverlayClick(e); 581 | 582 | // allow events within the message content 583 | if (target.parents('div.' + opts.blockMsgClass).length > 0) 584 | return true; 585 | 586 | // allow events for content that is not being blocked 587 | return target.parents().children().filter('div.blockUI').length === 0; 588 | } 589 | 590 | function focus(back) { 591 | if (!pageBlockEls) 592 | return; 593 | var e = pageBlockEls[back===true ? pageBlockEls.length-1 : 0]; 594 | if (e) 595 | e.focus(); 596 | } 597 | 598 | function center(el, x, y) { 599 | var p = el.parentNode, s = el.style; 600 | var l = ((p.offsetWidth - el.offsetWidth)/2) - sz(p,'borderLeftWidth'); 601 | var t = ((p.offsetHeight - el.offsetHeight)/2) - sz(p,'borderTopWidth'); 602 | if (x) s.left = l > 0 ? (l+'px') : '0'; 603 | if (y) s.top = t > 0 ? (t+'px') : '0'; 604 | } 605 | 606 | function sz(el, p) { 607 | return parseInt($.css(el,p),10)||0; 608 | } 609 | } 610 | 611 | /*global define:true */ 612 | if (typeof define === 'function' && define.amd && define.amd.jQuery) { 613 | define(['jquery'], setup); 614 | } else { 615 | setup(jQuery); 616 | } 617 | 618 | })(); 619 | -------------------------------------------------------------------------------- /src/main/resources/public/console/js/lz-string.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Pieroxy 2 | // This work is free. You can redistribute it and/or modify it 3 | // under the terms of the WTFPL, Version 2 4 | // For more information see LICENSE.txt or http://www.wtfpl.net/ 5 | // 6 | // For more information, the home page: 7 | // http://pieroxy.net/blog/pages/lz-string/testing.html 8 | // 9 | // LZ-based compression algorithm, version 1.4.4 10 | var LZString = (function() { 11 | 12 | // private property 13 | var f = String.fromCharCode; 14 | var keyStrBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; 15 | var keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$"; 16 | var baseReverseDic = {}; 17 | 18 | function getBaseValue(alphabet, character) { 19 | if (!baseReverseDic[alphabet]) { 20 | baseReverseDic[alphabet] = {}; 21 | for (var i=0 ; i>> 8; 66 | buf[i*2+1] = current_value % 256; 67 | } 68 | return buf; 69 | }, 70 | 71 | //decompress from uint8array (UCS-2 big endian format) 72 | decompressFromUint8Array:function (compressed) { 73 | if (compressed===null || compressed===undefined){ 74 | return LZString.decompress(compressed); 75 | } else { 76 | var buf=new Array(compressed.length/2); // 2 bytes per character 77 | for (var i=0, TotalLen=buf.length; i> 1; 159 | } 160 | } else { 161 | value = 1; 162 | for (i=0 ; i> 1; 184 | } 185 | } 186 | context_enlargeIn--; 187 | if (context_enlargeIn == 0) { 188 | context_enlargeIn = Math.pow(2, context_numBits); 189 | context_numBits++; 190 | } 191 | delete context_dictionaryToCreate[context_w]; 192 | } else { 193 | value = context_dictionary[context_w]; 194 | for (i=0 ; i> 1; 204 | } 205 | 206 | 207 | } 208 | context_enlargeIn--; 209 | if (context_enlargeIn == 0) { 210 | context_enlargeIn = Math.pow(2, context_numBits); 211 | context_numBits++; 212 | } 213 | // Add wc to the dictionary. 214 | context_dictionary[context_wc] = context_dictSize++; 215 | context_w = String(context_c); 216 | } 217 | } 218 | 219 | // Output the code for w. 220 | if (context_w !== "") { 221 | if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate,context_w)) { 222 | if (context_w.charCodeAt(0)<256) { 223 | for (i=0 ; i> 1; 244 | } 245 | } else { 246 | value = 1; 247 | for (i=0 ; i> 1; 269 | } 270 | } 271 | context_enlargeIn--; 272 | if (context_enlargeIn == 0) { 273 | context_enlargeIn = Math.pow(2, context_numBits); 274 | context_numBits++; 275 | } 276 | delete context_dictionaryToCreate[context_w]; 277 | } else { 278 | value = context_dictionary[context_w]; 279 | for (i=0 ; i> 1; 289 | } 290 | 291 | 292 | } 293 | context_enlargeIn--; 294 | if (context_enlargeIn == 0) { 295 | context_enlargeIn = Math.pow(2, context_numBits); 296 | context_numBits++; 297 | } 298 | } 299 | 300 | // Mark the end of the stream 301 | value = 2; 302 | for (i=0 ; i> 1; 312 | } 313 | 314 | // Flush the last char 315 | while (true) { 316 | context_data_val = (context_data_val << 1); 317 | if (context_data_position == bitsPerChar-1) { 318 | context_data.push(getCharFromInt(context_data_val)); 319 | break; 320 | } 321 | else context_data_position++; 322 | } 323 | return context_data.join(''); 324 | }, 325 | 326 | decompress: function (compressed) { 327 | if (compressed == null) return ""; 328 | if (compressed == "") return null; 329 | return LZString._decompress(compressed.length, 32768, function(index) { return compressed.charCodeAt(index); }); 330 | }, 331 | 332 | _decompress: function (length, resetValue, getNextValue) { 333 | var dictionary = [], 334 | next, 335 | enlargeIn = 4, 336 | dictSize = 4, 337 | numBits = 3, 338 | entry = "", 339 | result = [], 340 | i, 341 | w, 342 | bits, resb, maxpower, power, 343 | c, 344 | data = {val:getNextValue(0), position:resetValue, index:1}; 345 | 346 | for (i = 0; i < 3; i += 1) { 347 | dictionary[i] = i; 348 | } 349 | 350 | bits = 0; 351 | maxpower = Math.pow(2,2); 352 | power=1; 353 | while (power!=maxpower) { 354 | resb = data.val & data.position; 355 | data.position >>= 1; 356 | if (data.position == 0) { 357 | data.position = resetValue; 358 | data.val = getNextValue(data.index++); 359 | } 360 | bits |= (resb>0 ? 1 : 0) * power; 361 | power <<= 1; 362 | } 363 | 364 | switch (next = bits) { 365 | case 0: 366 | bits = 0; 367 | maxpower = Math.pow(2,8); 368 | power=1; 369 | while (power!=maxpower) { 370 | resb = data.val & data.position; 371 | data.position >>= 1; 372 | if (data.position == 0) { 373 | data.position = resetValue; 374 | data.val = getNextValue(data.index++); 375 | } 376 | bits |= (resb>0 ? 1 : 0) * power; 377 | power <<= 1; 378 | } 379 | c = f(bits); 380 | break; 381 | case 1: 382 | bits = 0; 383 | maxpower = Math.pow(2,16); 384 | power=1; 385 | while (power!=maxpower) { 386 | resb = data.val & data.position; 387 | data.position >>= 1; 388 | if (data.position == 0) { 389 | data.position = resetValue; 390 | data.val = getNextValue(data.index++); 391 | } 392 | bits |= (resb>0 ? 1 : 0) * power; 393 | power <<= 1; 394 | } 395 | c = f(bits); 396 | break; 397 | case 2: 398 | return ""; 399 | } 400 | dictionary[3] = c; 401 | w = c; 402 | result.push(c); 403 | while (true) { 404 | if (data.index > length) { 405 | return ""; 406 | } 407 | 408 | bits = 0; 409 | maxpower = Math.pow(2,numBits); 410 | power=1; 411 | while (power!=maxpower) { 412 | resb = data.val & data.position; 413 | data.position >>= 1; 414 | if (data.position == 0) { 415 | data.position = resetValue; 416 | data.val = getNextValue(data.index++); 417 | } 418 | bits |= (resb>0 ? 1 : 0) * power; 419 | power <<= 1; 420 | } 421 | 422 | switch (c = bits) { 423 | case 0: 424 | bits = 0; 425 | maxpower = Math.pow(2,8); 426 | power=1; 427 | while (power!=maxpower) { 428 | resb = data.val & data.position; 429 | data.position >>= 1; 430 | if (data.position == 0) { 431 | data.position = resetValue; 432 | data.val = getNextValue(data.index++); 433 | } 434 | bits |= (resb>0 ? 1 : 0) * power; 435 | power <<= 1; 436 | } 437 | 438 | dictionary[dictSize++] = f(bits); 439 | c = dictSize-1; 440 | enlargeIn--; 441 | break; 442 | case 1: 443 | bits = 0; 444 | maxpower = Math.pow(2,16); 445 | power=1; 446 | while (power!=maxpower) { 447 | resb = data.val & data.position; 448 | data.position >>= 1; 449 | if (data.position == 0) { 450 | data.position = resetValue; 451 | data.val = getNextValue(data.index++); 452 | } 453 | bits |= (resb>0 ? 1 : 0) * power; 454 | power <<= 1; 455 | } 456 | dictionary[dictSize++] = f(bits); 457 | c = dictSize-1; 458 | enlargeIn--; 459 | break; 460 | case 2: 461 | return result.join(''); 462 | } 463 | 464 | if (enlargeIn == 0) { 465 | enlargeIn = Math.pow(2, numBits); 466 | numBits++; 467 | } 468 | 469 | if (dictionary[c]) { 470 | entry = dictionary[c]; 471 | } else { 472 | if (c === dictSize) { 473 | entry = w + w.charAt(0); 474 | } else { 475 | return null; 476 | } 477 | } 478 | result.push(entry); 479 | 480 | // Add w+entry[0] to the dictionary. 481 | dictionary[dictSize++] = w + entry.charAt(0); 482 | enlargeIn--; 483 | 484 | w = entry; 485 | 486 | if (enlargeIn == 0) { 487 | enlargeIn = Math.pow(2, numBits); 488 | numBits++; 489 | } 490 | 491 | } 492 | } 493 | }; 494 | return LZString; 495 | })(); 496 | 497 | if (typeof define === 'function' && define.amd) { 498 | define(function () { return LZString; }); 499 | } else if( typeof module !== 'undefined' && module != null ) { 500 | module.exports = LZString 501 | } else if( typeof angular !== 'undefined' && angular != null ) { 502 | angular.module('LZString', []) 503 | .factory('LZString', function () { 504 | return LZString; 505 | }); 506 | } 507 | -------------------------------------------------------------------------------- /src/main/resources/public/console/js/matchbrackets.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: http://codemirror.net/LICENSE 3 | 4 | (function(mod) { 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS 6 | mod(require("../../lib/codemirror")); 7 | else if (typeof define == "function" && define.amd) // AMD 8 | define(["../../lib/codemirror"], mod); 9 | else // Plain browser env 10 | mod(CodeMirror); 11 | })(function(CodeMirror) { 12 | var ie_lt8 = /MSIE \d/.test(navigator.userAgent) && 13 | (document.documentMode == null || document.documentMode < 8); 14 | 15 | var Pos = CodeMirror.Pos; 16 | 17 | var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"}; 18 | 19 | function findMatchingBracket(cm, where, config) { 20 | var line = cm.getLineHandle(where.line), pos = where.ch - 1; 21 | var afterCursor = config && config.afterCursor 22 | if (afterCursor == null) 23 | afterCursor = /(^| )cm-fat-cursor($| )/.test(cm.getWrapperElement().className) 24 | 25 | // A cursor is defined as between two characters, but in in vim command mode 26 | // (i.e. not insert mode), the cursor is visually represented as a 27 | // highlighted box on top of the 2nd character. Otherwise, we allow matches 28 | // from before or after the cursor. 29 | var match = (!afterCursor && pos >= 0 && matching[line.text.charAt(pos)]) || 30 | matching[line.text.charAt(++pos)]; 31 | if (!match) return null; 32 | var dir = match.charAt(1) == ">" ? 1 : -1; 33 | if (config && config.strict && (dir > 0) != (pos == where.ch)) return null; 34 | var style = cm.getTokenTypeAt(Pos(where.line, pos + 1)); 35 | 36 | var found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style || null, config); 37 | if (found == null) return null; 38 | return {from: Pos(where.line, pos), to: found && found.pos, 39 | match: found && found.ch == match.charAt(0), forward: dir > 0}; 40 | } 41 | 42 | // bracketRegex is used to specify which type of bracket to scan 43 | // should be a regexp, e.g. /[[\]]/ 44 | // 45 | // Note: If "where" is on an open bracket, then this bracket is ignored. 46 | // 47 | // Returns false when no bracket was found, null when it reached 48 | // maxScanLines and gave up 49 | function scanForBracket(cm, where, dir, style, config) { 50 | var maxScanLen = (config && config.maxScanLineLength) || 10000; 51 | var maxScanLines = (config && config.maxScanLines) || 1000; 52 | 53 | var stack = []; 54 | var re = config && config.bracketRegex ? config.bracketRegex : /[(){}[\]]/; 55 | var lineEnd = dir > 0 ? Math.min(where.line + maxScanLines, cm.lastLine() + 1) 56 | : Math.max(cm.firstLine() - 1, where.line - maxScanLines); 57 | for (var lineNo = where.line; lineNo != lineEnd; lineNo += dir) { 58 | var line = cm.getLine(lineNo); 59 | if (!line) continue; 60 | var pos = dir > 0 ? 0 : line.length - 1, end = dir > 0 ? line.length : -1; 61 | if (line.length > maxScanLen) continue; 62 | if (lineNo == where.line) pos = where.ch - (dir < 0 ? 1 : 0); 63 | for (; pos != end; pos += dir) { 64 | var ch = line.charAt(pos); 65 | if (re.test(ch) && (style === undefined || cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style)) { 66 | var match = matching[ch]; 67 | if ((match.charAt(1) == ">") == (dir > 0)) stack.push(ch); 68 | else if (!stack.length) return {pos: Pos(lineNo, pos), ch: ch}; 69 | else stack.pop(); 70 | } 71 | } 72 | } 73 | return lineNo - dir == (dir > 0 ? cm.lastLine() : cm.firstLine()) ? false : null; 74 | } 75 | 76 | function matchBrackets(cm, autoclear, config) { 77 | // Disable brace matching in long lines, since it'll cause hugely slow updates 78 | var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000; 79 | var marks = [], ranges = cm.listSelections(); 80 | for (var i = 0; i < ranges.length; i++) { 81 | var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, config); 82 | if (match && cm.getLine(match.from.line).length <= maxHighlightLen) { 83 | var style = match.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket"; 84 | marks.push(cm.markText(match.from, Pos(match.from.line, match.from.ch + 1), {className: style})); 85 | if (match.to && cm.getLine(match.to.line).length <= maxHighlightLen) 86 | marks.push(cm.markText(match.to, Pos(match.to.line, match.to.ch + 1), {className: style})); 87 | } 88 | } 89 | 90 | if (marks.length) { 91 | // Kludge to work around the IE bug from issue #1193, where text 92 | // input stops going to the textare whever this fires. 93 | if (ie_lt8 && cm.state.focused) cm.focus(); 94 | 95 | var clear = function() { 96 | cm.operation(function() { 97 | for (var i = 0; i < marks.length; i++) marks[i].clear(); 98 | }); 99 | }; 100 | if (autoclear) setTimeout(clear, 800); 101 | else return clear; 102 | } 103 | } 104 | 105 | var currentlyHighlighted = null; 106 | function doMatchBrackets(cm) { 107 | cm.operation(function() { 108 | if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;} 109 | currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets); 110 | }); 111 | } 112 | 113 | CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) { 114 | if (old && old != CodeMirror.Init) { 115 | cm.off("cursorActivity", doMatchBrackets); 116 | if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;} 117 | } 118 | if (val) { 119 | cm.state.matchBrackets = typeof val == "object" ? val : {}; 120 | cm.on("cursorActivity", doMatchBrackets); 121 | } 122 | }); 123 | 124 | CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);}); 125 | CodeMirror.defineExtension("findMatchingBracket", function(pos, config, oldConfig){ 126 | // Backwards-compatibility kludge 127 | if (oldConfig || typeof config == "boolean") { 128 | if (!oldConfig) { 129 | config = config ? {strict: true} : null 130 | } else { 131 | oldConfig.strict = config 132 | config = oldConfig 133 | } 134 | } 135 | return findMatchingBracket(this, pos, config) 136 | }); 137 | CodeMirror.defineExtension("scanForBracket", function(pos, dir, style, config){ 138 | return scanForBracket(this, pos, dir, style, config); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /src/test/java/com/github/gaborbata/console/configuration/JGivenConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.gaborbata.console.configuration; 2 | 3 | import com.tngtech.jgiven.config.AbstractJGivenConfiguration; 4 | import com.tngtech.jgiven.integration.spring.EnableJGiven; 5 | import org.springframework.context.annotation.ComponentScan; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.http.HttpStatus; 8 | 9 | import static java.lang.String.format; 10 | 11 | /** 12 | * Configuration class for JGiven tests. 13 | */ 14 | @Configuration 15 | @EnableJGiven 16 | @ComponentScan("com.github.gaborbata.console") 17 | public class JGivenConfig extends AbstractJGivenConfiguration { 18 | 19 | @Override 20 | public void configure() { 21 | setFormatter(HttpStatus.class, (status, annotations) -> format("%s (%d)", status.getReasonPhrase(), status.value())); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/com/github/gaborbata/console/controller/GroovyConsoleControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.github.gaborbata.console.controller; 2 | 3 | import com.github.gaborbata.console.configuration.JGivenConfig; 4 | import com.github.gaborbata.console.controller.stage.GroovyConsoleControllerStage; 5 | import com.tngtech.jgiven.integration.spring.junit5.SimpleSpringScenarioTest; 6 | import com.tngtech.jgiven.junit5.JGivenExtension; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; 11 | import org.springframework.http.HttpStatus; 12 | 13 | @ExtendWith(JGivenExtension.class) 14 | @SpringBootTest(classes = {JGivenConfig.class}, webEnvironment = WebEnvironment.NONE) 15 | public class GroovyConsoleControllerTest extends SimpleSpringScenarioTest { 16 | 17 | private static final String GROOVY_CONSOLE_ENDPOINT = "/console/groovy"; 18 | 19 | @Test 20 | public void should_include_script_return_result_in_the_result_object() throws Exception { 21 | given().an_endpoint(GROOVY_CONSOLE_ENDPOINT) 22 | .and().a_groovy_script_$script("2 + 3"); 23 | when().post_request_is_received(); 24 | then().the_status_is(HttpStatus.OK) 25 | .and().the_content_is("{'result': 5}"); 26 | } 27 | 28 | @Test 29 | public void should_include_script_output_in_the_result_object() throws Exception { 30 | given().an_endpoint(GROOVY_CONSOLE_ENDPOINT) 31 | .and().a_groovy_script_$script("println 2 + 3"); 32 | when().post_request_is_received(); 33 | then().the_status_is(HttpStatus.OK) 34 | .and().the_content_is("{'output': ['5']}"); 35 | } 36 | 37 | @Test 38 | public void should_include_both_output_and_result_object_in_the_result() throws Exception { 39 | given().an_endpoint(GROOVY_CONSOLE_ENDPOINT) 40 | .and().a_groovy_script_$script("println('test'); 2 + 3"); 41 | when().post_request_is_received(); 42 | then().the_status_is(HttpStatus.OK) 43 | .and().the_content_is("{'output': ['test'], 'result': 5}"); 44 | } 45 | 46 | @Test 47 | public void should_respond_with_bad_request_when_script_is_missing() throws Exception { 48 | given().an_endpoint(GROOVY_CONSOLE_ENDPOINT) 49 | .and().a_groovy_script_$script(null); 50 | when().post_request_is_received(); 51 | then().the_status_is(HttpStatus.BAD_REQUEST); 52 | } 53 | 54 | @Test 55 | public void should_include_spring_application_context_in_bound_variables() throws Exception { 56 | given().an_endpoint(GROOVY_CONSOLE_ENDPOINT) 57 | .and().a_groovy_script_$script("applicationContext != null"); 58 | when().post_request_is_received(); 59 | then().the_status_is(HttpStatus.OK) 60 | .and().the_content_is("{'result': true}"); 61 | } 62 | 63 | @Test 64 | public void should_include_exception_message_in_the_result() throws Exception { 65 | given().an_endpoint(GROOVY_CONSOLE_ENDPOINT) 66 | .and().a_groovy_script_$script("throw new RuntimeException('test')"); 67 | when().post_request_is_received(); 68 | then().the_status_is(HttpStatus.OK) 69 | .and().the_content_is("{'output': ['java.lang.RuntimeException: test']}"); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/com/github/gaborbata/console/controller/stage/GroovyConsoleControllerStage.java: -------------------------------------------------------------------------------- 1 | package com.github.gaborbata.console.controller.stage; 2 | 3 | import com.github.gaborbata.console.controller.GroovyConsoleController; 4 | import com.tngtech.jgiven.Stage; 5 | import com.tngtech.jgiven.annotation.BeforeStage; 6 | import com.tngtech.jgiven.annotation.Quoted; 7 | import com.tngtech.jgiven.integration.spring.JGivenStage; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.test.web.servlet.MockMvc; 11 | import org.springframework.test.web.servlet.ResultActions; 12 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 13 | 14 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; 15 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 16 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 17 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; 18 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 19 | 20 | @JGivenStage 21 | public class GroovyConsoleControllerStage extends Stage { 22 | 23 | @Autowired 24 | private GroovyConsoleController groovyConsoleController; 25 | 26 | private MockMvc mockMvc; 27 | 28 | private String uri; 29 | private String script; 30 | private ResultActions resultActions; 31 | 32 | @BeforeStage 33 | public void setup() { 34 | mockMvc = MockMvcBuilders.standaloneSetup(groovyConsoleController).build(); 35 | } 36 | 37 | public GroovyConsoleControllerStage an_endpoint(@Quoted String uri) { 38 | this.uri = uri; 39 | return this; 40 | } 41 | 42 | public GroovyConsoleControllerStage a_groovy_script_$script(@Quoted String script) { 43 | this.script = script; 44 | return this; 45 | } 46 | 47 | public GroovyConsoleControllerStage post_request_is_received() throws Exception { 48 | ResultActions resultActions = mockMvc.perform(post(uri).contentType("text/x-groovy").param("script", script)); 49 | if (script == null) { 50 | this.resultActions = resultActions; 51 | } else { 52 | this.resultActions = mockMvc.perform(asyncDispatch(resultActions.andExpect(request().asyncStarted()).andReturn())); 53 | } 54 | return this; 55 | } 56 | 57 | public GroovyConsoleControllerStage the_status_is(HttpStatus status) throws Exception { 58 | resultActions.andExpect(status().is(status.value())); 59 | return this; 60 | } 61 | 62 | public GroovyConsoleControllerStage the_content_is(String content) throws Exception { 63 | resultActions.andExpect(content().json(content)); 64 | return this; 65 | } 66 | } 67 | --------------------------------------------------------------------------------