├── .gitignore ├── .scalafmt.conf ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets ├── scalafiddle-logo.ai ├── scalafiddle-logo.png ├── scalafiddle-newlogo.ai └── smooth-spiral.png ├── build.sbt ├── client └── src │ ├── main │ └── scala │ │ └── scalafiddle │ │ └── client │ │ ├── AjaxClient.scala │ │ ├── AppCircuit.scala │ │ ├── AppMain.scala │ │ ├── AppModel.scala │ │ ├── AppRouter.scala │ │ ├── CompilerHandler.scala │ │ ├── FiddleHandler.scala │ │ ├── LoginHandler.scala │ │ ├── Utils.scala │ │ └── component │ │ ├── Dropdown.scala │ │ ├── EmbedEditor.scala │ │ ├── FiddleEditor.scala │ │ ├── Icon.scala │ │ ├── JQuery.scala │ │ ├── SemanticUI.scala │ │ ├── Sidebar.scala │ │ └── UserLogin.scala │ └── test │ └── scala │ └── scalafiddle │ └── client │ └── DummySpec.scala ├── docs ├── Welcome-src.md ├── Welcome.html ├── _config.yml ├── _layouts │ └── default.html ├── assets │ └── css │ │ └── style.scss ├── embed.png ├── libraries.png └── showtemplate.png ├── project ├── Settings.scala ├── build.properties └── plugins.sbt ├── server └── src │ ├── main │ ├── assets │ │ ├── images │ │ │ ├── anon.png │ │ │ ├── favicon-114.png │ │ │ ├── favicon-120.png │ │ │ ├── favicon-144.png │ │ │ ├── favicon-150.png │ │ │ ├── favicon-152.png │ │ │ ├── favicon-16.png │ │ │ ├── favicon-160.png │ │ │ ├── favicon-180.png │ │ │ ├── favicon-192.png │ │ │ ├── favicon-310.png │ │ │ ├── favicon-32.png │ │ │ ├── favicon-57.png │ │ │ ├── favicon-60.png │ │ │ ├── favicon-64.png │ │ │ ├── favicon-70.png │ │ │ ├── favicon-72.png │ │ │ ├── favicon-76.png │ │ │ ├── favicon-96.png │ │ │ ├── favicon.ico │ │ │ ├── providers │ │ │ │ ├── facebook.png │ │ │ │ ├── github.png │ │ │ │ ├── google.png │ │ │ │ ├── twitter.png │ │ │ │ ├── vk.png │ │ │ │ ├── xing.png │ │ │ │ └── yahoo.png │ │ │ ├── scalafiddle-logo-small.png │ │ │ ├── scalafiddle-logo.png │ │ │ └── wait-ring.gif │ │ ├── javascript │ │ │ ├── gzip.js │ │ │ └── mousetrap.js │ │ └── stylesheets │ │ │ └── main.less │ ├── resources │ │ ├── application.conf │ │ ├── logback.xml │ │ ├── routes │ │ ├── silhouette.conf │ │ └── tables.sql │ ├── scala │ │ ├── ErrorHandler.scala │ │ ├── controllers │ │ │ ├── Application.scala │ │ │ └── SocialAuthController.scala │ │ └── scalafiddle │ │ │ └── server │ │ │ ├── ApiService.scala │ │ │ ├── Filters.scala │ │ │ ├── HTMLFiddle.scala │ │ │ ├── Highlighter.scala │ │ │ ├── Librarian.scala │ │ │ ├── Persistence.scala │ │ │ ├── dao │ │ │ ├── AccessDAO.scala │ │ │ ├── DriverComponent.scala │ │ │ ├── FiddleDAL.scala │ │ │ ├── FiddleDAO.scala │ │ │ └── UserDAO.scala │ │ │ ├── models │ │ │ ├── AuthToken.scala │ │ │ ├── User.scala │ │ │ └── services │ │ │ │ ├── UserService.scala │ │ │ │ └── UserServiceImpl.scala │ │ │ ├── modules │ │ │ └── SilhouetteModule.scala │ │ │ └── utils │ │ │ └── auth │ │ │ ├── AllLoginProviders.scala │ │ │ ├── Env.scala │ │ │ └── WithProvider.scala │ └── twirl │ │ └── views │ │ ├── index.scala.html │ │ ├── listfiddles.scala.html │ │ └── resultframe.scala.html │ └── test │ ├── resources │ └── test-embed.html │ └── scala │ └── scalafiddle │ └── server │ └── HighlighterSpec.scala └── shared └── src └── main └── scala └── scalafiddle └── shared ├── Api.scala └── FiddleData.scala /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | *.sc 4 | 5 | # sbt specific 6 | .cache 7 | .history 8 | .lib/ 9 | dist/* 10 | target/ 11 | lib_managed/ 12 | src_managed/ 13 | project/boot/ 14 | project/plugins/project/ 15 | 16 | # Scala-IDE specific 17 | .scala_dependencies 18 | .worksheet 19 | .idea 20 | temp/* 21 | /server/src/main/resources/local.conf 22 | 23 | node_modules 24 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 2.3.2 2 | style = default 3 | align = more 4 | maxColumn = 125 5 | rewrite.rules = [SortImports] 6 | project.git = true 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | scala: 3 | - 2.12.10 4 | sudo: false 5 | jdk: 6 | - openjdk8 7 | script: 8 | - sbt ++$TRAVIS_SCALA_VERSION server/test client/test 9 | cache: 10 | directories: 11 | - $HOME/.cache/coursier 12 | - $HOME/.ivy2/cache 13 | - $HOME/.sbt 14 | install: 15 | - . $HOME/.nvm/nvm.sh 16 | - nvm install stable 17 | - nvm use stable 18 | - npm install 19 | - npm install jsdom@9.12.0 20 | 21 | # Tricks to avoid unnecessary cache updates, from 22 | # https://www.scala-sbt.org/1.x/docs/Travis-CI-with-sbt.html#Caching 23 | before_cache: 24 | - rm -fv $HOME/.ivy2/.sbt.ivy.lock 25 | - find $HOME/.ivy2/cache -name "ivydata-*.properties" -print -delete 26 | - find $HOME/.sbt -name "*.lock" -print -delete 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ScalaFiddle is open-source software and relies on individual contributions to keep it up-to-date. Please read through this (short) guide to make sure your 4 | contributions can be easily integrated into the project. 5 | 6 | ## Setting up dev env locally 7 | 8 | ScalaFiddle has two separate repositories, the editor (this one) and the [core](https://github.com/scalafiddle/scalafiddle-core). To get started, clone the two 9 | repos locally. 10 | 11 | ```bash 12 | $ git clone https://github.com/scalafiddle/scalafiddle-core.git 13 | 14 | $ git clone https://github.com/scalafiddle/scalafiddle-editor.git 15 | ``` 16 | 17 | Next start two separate `sbt` sessions in two terminal windows, one for each project. 18 | 19 | You can now start the editor with 20 | ``` 21 | [server] run 22 | ``` 23 | which will launch the Play framework, waiting for a connection on 0.0.0.0:9000. At this point you can already navigate to http://localhost:9000 and use the 24 | editor. 25 | 26 | If you get errors like `Cannot run program "node": Malformed argument has embedded quote`, you may need to add a 27 | JVM configuration option and restart `sbt`. 28 | ``` 29 | export JAVA_TOOL_OPTIONS=-Djdk.lang.Process.allowAmbiguousCommands=true 30 | ``` 31 | 32 | Before you can compile anything, you must also start the `router` and `compilerServer` in the other `sbt` session. These are built on Akka-HTTP instead of Play, 33 | so you can run them in the background with 34 | 35 | ``` 36 | > router/reStart 37 | 38 | > compilerServer/reStart 39 | ``` 40 | 41 | The `router` will load library information from the same place as `editor` does. `compilerServer` connects to the `router` over a web-socket and 42 | received commands from the `router`. On the first run the compiler server will download all libraries and build a large _flat file_ containing classes and sjsir 43 | files from those libraries. This will take some time, but is only done once. Afterwards the _flat file_ is reused and new libraries are only appended to it. 44 | 45 | Now your dev env should be running and you should be able to create, edit, compile and run fiddles! Note that saved fiddles will disappear when the `editor` is 46 | stopped, as by default they are stored in an in-memory H2 database. 47 | 48 | ## Supporting new libraries 49 | 50 | By far the most common contribution is adding support for a new library (or a version of a library). To do this you simply need to edit the 51 | [libraries.json](https://github.com/scalafiddle/scalafiddle-io/blob/master/libraries.json) file under the 52 | [scalafiddle-io](https://github.com/scalafiddle/scalafiddle-io) repo. 53 | 54 | To test new libraries locally, override the default library URL configuration via environment variable `SCALAFIDDLE_LIBRARIES_URL` 55 | to point to a local file using `file:/path/to/local/libraries.json` syntax. 56 | 57 | Please follow these rules for adding a new library: 58 | 59 | 1. Never remove old versions of libraries 60 | 1. When adding a new version, make it the first in the list of versions 61 | 1. A new library should be added under an appropriate group. If no group seems to match, contact the maintainers. 62 | 1. Remember to fill in all fields, including the doc link (which can be a Github project name or a full URL) 63 | 1. Test the newly added library in your local ScalaFiddle editor, using the library and its features. 64 | 65 | When everything works locally, you can submit a PR with your changes to the main repo. 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ScalaFiddle editor 2 | 3 | This project provides an [online editor](https://scalafiddle.io) for creating, sharing and embedding Scala fiddles (little Scala programs that can run in your 4 | browser). 5 | 6 | ## Usage 7 | 8 | For documentation on how to use the ScalaFiddle editor, see the [Welcome](docs/Welcome.md). 9 | 10 | ## Hosting your own ScalaFiddle editor 11 | 12 | ScalaFiddle has been designed to be easy to host on your own. It's published as Docker images (`scalafiddle/scalafiddle-editor`, 13 | `scalafiddle/scalafiddle-router` and `scalafiddle/scalafiddle-core`) which provide an easy way to set up your own service. 14 | 15 | ## Contributing 16 | 17 | If you'd like to contribute to the ScalaFiddle project, for example to add a new library dependency, read the [contribution guide](CONTRIBUTING.md) 18 | for more instructions on how to set up a local dev env for ScalaFiddle editor and core. 19 | -------------------------------------------------------------------------------- /assets/scalafiddle-logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/assets/scalafiddle-logo.ai -------------------------------------------------------------------------------- /assets/scalafiddle-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/assets/scalafiddle-logo.png -------------------------------------------------------------------------------- /assets/scalafiddle-newlogo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/assets/scalafiddle-newlogo.ai -------------------------------------------------------------------------------- /assets/smooth-spiral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/assets/smooth-spiral.png -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import sbt.Keys._ 2 | import sbt.Project.projectToRef 3 | import org.scalajs.sbtplugin.ScalaJSPlugin 4 | import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ 5 | import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType} 6 | 7 | ThisBuild / scalafmtOnCompile := true 8 | 9 | resolvers in ThisBuild += Resolver.jcenterRepo 10 | 11 | // a special crossProject for configuring a JS/JVM/shared structure 12 | lazy val shared = (crossProject(JSPlatform, JVMPlatform).crossType(CrossType.Pure) in file("shared")) 13 | .settings( 14 | version := Settings.version, 15 | scalaVersion := Settings.versions.scala, 16 | libraryDependencies ++= Settings.sharedDependencies.value 17 | ) 18 | // set up settings specific to the JS project 19 | .jsConfigure(_ enablePlugins ScalaJSWeb) 20 | 21 | lazy val sharedJVM = shared.jvm.settings(name := "sharedJVM") 22 | 23 | lazy val sharedJS = shared.js.settings(name := "sharedJS") 24 | 25 | // instantiate the JS project for SBT with some additional settings 26 | lazy val client = (project in file("client")) 27 | .settings( 28 | name := "client", 29 | version := Settings.version, 30 | scalaVersion := Settings.versions.scala, 31 | scalacOptions ++= Settings.scalacOptions, 32 | libraryDependencies ++= Settings.sharedDependencies.value ++ Settings.scalajsDependencies.value, 33 | jsDependencies ++= Settings.jsDependencies.value, 34 | jsEnv := new org.scalajs.jsenv.jsdomnodejs.JSDOMNodeJSEnv(), 35 | // yes, we want to package JS dependencies 36 | skip in packageJSDependencies := false, 37 | // use Scala.js provided launcher code to start the client app 38 | scalaJSUseMainModuleInitializer := true 39 | ) 40 | .enablePlugins(ScalaJSPlugin, ScalaJSWeb) 41 | .dependsOn(sharedJS) 42 | 43 | // Client projects (just one in this case) 44 | lazy val clients = Seq(client) 45 | 46 | // instantiate the JVM project for SBT with some additional settings 47 | lazy val server = (project in file("server")) 48 | .settings( 49 | name := "server", 50 | version := Settings.version, 51 | scalaVersion := Settings.versions.scala, 52 | scalacOptions ++= Settings.scalacOptions, 53 | libraryDependencies ++= Settings.sharedDependencies.value ++ Settings.jvmDependencies.value ++ Seq( 54 | filters, 55 | guice, 56 | ehcache 57 | ), 58 | // connect to the client project 59 | scalaJSProjects := clients, 60 | pipelineStages in Assets := Seq(scalaJSPipeline), 61 | pipelineStages := Seq(digest, gzip), 62 | // compress CSS 63 | LessKeys.compress in Assets := true, 64 | scriptClasspath := Seq("../config/") ++ scriptClasspath.value, 65 | mappings in Universal := (mappings in Universal).value.filter { 66 | case (file, fileName) => !fileName.endsWith("local.conf") 67 | }, 68 | mappings in (Compile, packageDoc) := Seq(), 69 | javaOptions in Universal ++= Seq( 70 | "-Dpidfile.path=/dev/null" 71 | ), 72 | dockerfile in docker := { 73 | val appDir: File = stage.value 74 | val targetDir = "/app" 75 | 76 | new Dockerfile { 77 | from("anapsix/alpine-java:8_jdk") 78 | run("apk", "add", "--update", "bash") 79 | entryPoint(s"$targetDir/bin/${executableScriptName.value}") 80 | copy(appDir, targetDir) 81 | expose(8080) 82 | } 83 | }, 84 | imageNames in docker := Seq( 85 | ImageName( 86 | namespace = Some("scalafiddle"), 87 | repository = "scalafiddle-editor", 88 | tag = Some("latest") 89 | ), 90 | ImageName( 91 | namespace = Some("scalafiddle"), 92 | repository = "scalafiddle-editor", 93 | tag = Some(version.value) 94 | ) 95 | ) 96 | ) 97 | .enablePlugins(PlayScala, SbtWeb, sbtdocker.DockerPlugin) 98 | .disablePlugins(PlayLayoutPlugin) // use the standard directory layout instead of Play's custom 99 | .aggregate(clients.map(projectToRef): _*) 100 | .dependsOn(sharedJVM) 101 | 102 | // loads the Play server project at sbt startup 103 | onLoad in Global := (Command.process("project server", _: State)) compose (onLoad in Global).value 104 | -------------------------------------------------------------------------------- /client/src/main/scala/scalafiddle/client/AjaxClient.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.client 2 | 3 | import org.scalajs.dom 4 | import upickle.Js 5 | import upickle.default._ 6 | 7 | import scala.concurrent.ExecutionContext.Implicits.global 8 | import scala.concurrent.Future 9 | 10 | object AjaxClient extends autowire.Client[Js.Value, Reader, Writer] { 11 | override def doCall(req: Request): Future[Js.Value] = { 12 | dom.ext.Ajax 13 | .post( 14 | url = "/api/" + req.path.mkString("/"), 15 | data = upickle.json.write(Js.Obj(req.args.toSeq: _*)) 16 | ) 17 | .map(_.responseText) 18 | .map(upickle.json.read) 19 | } 20 | 21 | def read[Result: Reader](p: Js.Value) = readJs[Result](p) 22 | def write[Result: Writer](r: Result) = writeJs(r) 23 | } 24 | -------------------------------------------------------------------------------- /client/src/main/scala/scalafiddle/client/AppCircuit.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.client 2 | 3 | import diode.ActionResult.{ModelUpdate, ModelUpdateSilent} 4 | import diode._ 5 | import diode.data.Empty 6 | import diode.react.ReactConnector 7 | import upickle.default._ 8 | 9 | import scala.scalajs.js 10 | import scala.scalajs.js.JSON 11 | import scalafiddle.client.AppRouter.Home 12 | import scalafiddle.shared._ 13 | 14 | object AppCircuit extends Circuit[AppModel] with ReactConnector[AppModel] { 15 | // load from global config 16 | def fiddleData = read[FiddleData](JSON.stringify(js.Dynamic.global.ScalaFiddleData)) 17 | 18 | override protected def initialModel = 19 | AppModel(Home, None, fiddleData, ScalaFiddleHelp(ScalaFiddleConfig.helpURL), LoginData(Empty, Empty)) 20 | 21 | override protected def actionHandler = composeHandlers( 22 | new FiddleHandler( 23 | zoomRW(_.fiddleData)((m, v) => m.copy(fiddleData = v)), 24 | zoomRW(_.fiddleId)((m, v) => m.copy(fiddleId = v)) 25 | ), 26 | new CompilerHandler(zoomRW(_.outputData)((m, v) => m.copy(outputData = v))), 27 | new LoginHandler(zoomRW(_.loginData)((m, v) => m.copy(loginData = v))), 28 | navigationHandler 29 | ) 30 | 31 | override def handleFatal(action: Any, e: Throwable): Unit = { 32 | println(s"Error handling action: $action") 33 | println(e.toString) 34 | } 35 | 36 | override def handleError(e: String): Unit = { 37 | println(s"Error in circuit: $e") 38 | } 39 | 40 | val navigationHandler: (AppModel, Any) => Option[ActionResult[AppModel]] = (model, action) => 41 | action match { 42 | case NavigateTo(page) => 43 | Some(ModelUpdate(model.copy(navLocation = page))) 44 | case NavigateSilentTo(page) => 45 | Some(ModelUpdateSilent(model.copy(navLocation = page))) 46 | case _ => 47 | None 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /client/src/main/scala/scalafiddle/client/AppMain.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.client 2 | 3 | import org.scalajs.dom 4 | 5 | import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel} 6 | 7 | @JSExportTopLevel("AppMain") 8 | object AppMain { 9 | @JSExport 10 | def main(args: Array[String]): Unit = { 11 | AppRouter.router().renderIntoDOM(dom.document.getElementById("root")) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/src/main/scala/scalafiddle/client/AppModel.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.client 2 | 3 | import diode._ 4 | import diode.data.Pot 5 | 6 | import scalafiddle.shared._ 7 | 8 | case class EditorAnnotation(row: Int, col: Int, text: Seq[String], tpe: String) 9 | 10 | sealed trait CompilerStatus { 11 | def show: String 12 | } 13 | 14 | object CompilerStatus { 15 | case object Result extends CompilerStatus { 16 | val show = "RESULT" 17 | } 18 | case object Compiling extends CompilerStatus { 19 | val show = "COMPILING" 20 | } 21 | case object Compiled extends CompilerStatus { 22 | val show = "COMPILED" 23 | } 24 | case object Running extends CompilerStatus { 25 | val show = "RUNNING" 26 | } 27 | case object Error extends CompilerStatus { 28 | val show = "ERROR" 29 | } 30 | } 31 | 32 | sealed trait OutputData 33 | 34 | case class CompilerData( 35 | status: CompilerStatus, 36 | jsCode: Option[String], 37 | jsDeps: Seq[String], 38 | cssDeps: Seq[String], 39 | annotations: Seq[EditorAnnotation], 40 | errorMessage: Option[String], 41 | log: String 42 | ) extends OutputData 43 | 44 | case class UserFiddleData( 45 | fiddles: Seq[FiddleVersions] 46 | ) extends OutputData 47 | 48 | case class ScalaFiddleHelp(url: String) extends OutputData 49 | 50 | case class LoginData( 51 | userInfo: Pot[UserInfo], 52 | loginProviders: Pot[Seq[LoginProvider]] 53 | ) 54 | 55 | case class AppModel( 56 | navLocation: Page, 57 | fiddleId: Option[FiddleId], 58 | fiddleData: FiddleData, 59 | outputData: OutputData, 60 | loginData: LoginData 61 | ) 62 | 63 | case class NavigateTo(page: Page) extends Action 64 | 65 | case class NavigateSilentTo(page: Page) extends Action 66 | -------------------------------------------------------------------------------- /client/src/main/scala/scalafiddle/client/AppRouter.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.client 2 | 3 | import diode._ 4 | import japgolly.scalajs.react.extra.router._ 5 | import org.scalajs.dom 6 | 7 | import scala.util.Try 8 | import scalafiddle.client.component.FiddleEditor 9 | import scalafiddle.shared.FiddleId 10 | 11 | sealed trait Page 12 | 13 | object AppRouter { 14 | 15 | case object Home extends Page 16 | 17 | case class EditorPage(id: String, version: Int) extends Page 18 | 19 | val fiddleData = AppCircuit.connect(_.fiddleData) 20 | 21 | val config = RouterConfigDsl[Page] 22 | .buildConfig { dsl => 23 | import dsl._ 24 | import japgolly.scalajs.react.vdom.Implicits._ 25 | 26 | (staticRoute(root, Home) ~> render( 27 | fiddleData(d => FiddleEditor(d, None, AppCircuit.zoom(_.outputData), AppCircuit.zoom(_.loginData))) 28 | ) 29 | | dynamicRouteF(("sf" / string("\\w+") / string(".+")).pmap[EditorPage] { path => 30 | Some(EditorPage(path._1, Try(path._2.takeWhile(_.isDigit).toInt).getOrElse(0))) 31 | }(page => (page.id, page.version.toString)))(p => Some(p.asInstanceOf[EditorPage])) ~> dynRender((p: EditorPage) => { 32 | val fid = FiddleId(p.id, p.version) 33 | AppCircuit.dispatch(UpdateId(fid, silent = true)) 34 | fiddleData(d => FiddleEditor(d, Some(fid), AppCircuit.zoom(_.outputData), AppCircuit.zoom(_.loginData))) 35 | })) 36 | .notFound(p => redirectToPage(Home)(Redirect.Replace)) 37 | } 38 | .renderWith(layout) 39 | 40 | val baseUrl = BaseUrl.fromWindowOrigin_/ 41 | 42 | val (router, routerLogic) = Router.componentAndLogic(baseUrl, config) 43 | 44 | def layout(c: RouterCtl[Page], r: Resolution[Page]) = { 45 | import japgolly.scalajs.react.vdom.all._ 46 | 47 | div(r.render()).render 48 | } 49 | 50 | def navigated(page: ModelRO[Page]): Unit = { 51 | // scalajs.js.timers.setTimeout(0)(routerLogic.ctl.set(page.value).runNow()) 52 | page() match { 53 | case Home => 54 | scalajs.js.timers.setTimeout(0)(dom.window.location.assign("/")) 55 | case EditorPage(id, version) => 56 | scalajs.js.timers.setTimeout(0)(dom.window.location.assign(s"/sf/$id/$version")) 57 | } 58 | } 59 | 60 | // subscribe to navigation changes 61 | AppCircuit.subscribe(AppCircuit.zoom(_.navLocation))(navigated) 62 | } 63 | -------------------------------------------------------------------------------- /client/src/main/scala/scalafiddle/client/CompilerHandler.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.client 2 | 3 | import java.nio.charset.StandardCharsets 4 | 5 | import autowire._ 6 | import diode._ 7 | import org.scalajs.dom 8 | import org.scalajs.dom.ext.Ajax 9 | import upickle.default._ 10 | 11 | import scala.scalajs.js 12 | import scala.concurrent.ExecutionContext.Implicits.global 13 | import scalafiddle.shared.{Api, FiddleVersions} 14 | 15 | sealed trait SJSOptimization { 16 | def flag: String 17 | } 18 | 19 | case object FastOpt extends SJSOptimization { 20 | val flag = "fast" 21 | } 22 | 23 | case object FullOpt extends SJSOptimization { 24 | val flag = "full" 25 | } 26 | 27 | case class CompileFiddle(source: String, optimization: SJSOptimization) extends Action 28 | 29 | case class AutoCompleteFiddle(source: String, row: Int, col: Int, callback: (Seq[(String, String)]) => Unit) extends Action 30 | 31 | case class CompilerSuccess( 32 | jsCode: String, 33 | jsDeps: Seq[String], 34 | cssDeps: Seq[String], 35 | log: String 36 | ) extends Action 37 | 38 | case class CompilerFailed( 39 | annotations: Seq[EditorAnnotation], 40 | log: String 41 | ) extends Action 42 | 43 | case class ServerError(message: String) extends Action 44 | 45 | case object LoadUserFiddles extends Action 46 | 47 | case class UserFiddlesUpdated(fiddles: Seq[FiddleVersions]) extends Action 48 | 49 | class CompilerHandler[M](modelRW: ModelRW[M, OutputData]) extends ActionHandler(modelRW) { 50 | 51 | case class CompilationResponse( 52 | jsCode: Option[String], 53 | jsDeps: Seq[String], 54 | cssDeps: Seq[String], 55 | annotations: Seq[EditorAnnotation], 56 | log: String 57 | ) 58 | 59 | case class CompletionResponse(completions: List[(String, String)]) 60 | 61 | override def handle = { 62 | case AutoCompleteFiddle(source, row, col, callback) => 63 | val effect = { 64 | val intOffset = col + source 65 | .split("\n") 66 | .take(row) 67 | .map(_.length + 1) 68 | .sum 69 | // call the ScalaFiddle compiler to perform completion 70 | Ajax 71 | .post( 72 | url = s"${ScalaFiddleConfig.compilerURL}/complete?offset=$intOffset", 73 | data = source 74 | ) 75 | .map { request => 76 | val results = read[CompletionResponse](request.responseText) 77 | println(s"Completion results: $results") 78 | callback(results.completions) 79 | NoAction 80 | } 81 | } 82 | effectOnly(Effect(effect)) 83 | 84 | case CompileFiddle(source, optimization) => 85 | val effect = Ajax 86 | .post( 87 | url = s"${ScalaFiddleConfig.compilerURL}/compile?opt=${optimization.flag}", 88 | data = source 89 | ) 90 | .map { request => 91 | read[CompilationResponse](request.responseText) match { 92 | case CompilationResponse(Some(jsCode), jsDeps, cssDeps, _, log) => 93 | CompilerSuccess(jsCode, jsDeps, cssDeps, log) 94 | case CompilationResponse(None, _, _, annotations, log) => 95 | CompilerFailed(annotations, log) 96 | } 97 | } recover { 98 | case e: dom.ext.AjaxException => 99 | e.xhr.status match { 100 | case 400 => 101 | ServerError(e.xhr.responseText) 102 | case x => 103 | ServerError(s"Server responded with $x: ${e.xhr.responseText}") 104 | } 105 | case e: Throwable => 106 | ServerError(s"Unknown error while compiling") 107 | } 108 | updated(CompilerData(CompilerStatus.Compiling, None, Nil, Nil, Nil, None, ""), Effect(effect)) 109 | 110 | case CompilerSuccess(jsCode, jsDeps, cssDeps, log) => 111 | updated(CompilerData(CompilerStatus.Compiled, Some(jsCode), jsDeps, cssDeps, Nil, None, log)) 112 | 113 | case CompilerFailed(annotations, log) => 114 | updated(CompilerData(CompilerStatus.Error, None, Nil, Nil, annotations, None, log)) 115 | 116 | case ServerError(message) => 117 | updated(CompilerData(CompilerStatus.Error, None, Nil, Nil, Nil, Some(message), "")) 118 | 119 | case LoadUserFiddles => 120 | effectOnly(Effect(AjaxClient[Api].listFiddles().call().map(UserFiddlesUpdated))) 121 | 122 | case UserFiddlesUpdated(fiddles) => 123 | updated(UserFiddleData(fiddles)) 124 | 125 | case ShowHelp(url) => 126 | updated(ScalaFiddleHelp(url)) 127 | } 128 | 129 | def encodeSource(source: String): String = { 130 | import com.github.marklister.base64.Base64._ 131 | import js.JSConverters._ 132 | implicit def scheme: B64Scheme = base64Url 133 | val fullSource = source.getBytes(StandardCharsets.UTF_8) 134 | val compressedBuffer = new Gzip(fullSource.toJSArray).compress() 135 | val compressedSource = new Array[Byte](compressedBuffer.length) 136 | var i = 0 137 | while (i < compressedBuffer.length) { 138 | compressedSource(i) = compressedBuffer.get(i).toByte 139 | i += 1 140 | } 141 | Encoder(compressedSource).toBase64 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /client/src/main/scala/scalafiddle/client/FiddleHandler.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.client 2 | 3 | import autowire._ 4 | import diode.ActionResult.{ModelUpdate, ModelUpdateEffect} 5 | import diode._ 6 | import org.scalajs.dom 7 | 8 | import scala.concurrent.ExecutionContext.Implicits.global 9 | import scala.concurrent.Future 10 | import scalafiddle.shared._ 11 | 12 | case class SelectLibrary(lib: Library) extends Action 13 | 14 | case class DeselectLibrary(lib: Library) extends Action 15 | 16 | case class SelectScalaVersion(version: String) extends Action 17 | 18 | case class SelectScalaJSVersion(version: String) extends Action 19 | 20 | case class UpdateInfo(name: String, description: String) extends Action 21 | 22 | case class UpdateSource(source: String) extends Action 23 | 24 | case class SaveFiddle(source: String) extends Action 25 | 26 | case class UpdateFiddle(source: String) extends Action 27 | 28 | case class ForkFiddle(source: String) extends Action 29 | 30 | case class LoadFiddle(url: String) extends Action 31 | 32 | case class UpdateId(fiddleId: FiddleId, silent: Boolean = false) extends Action 33 | 34 | case class ShowHelp(url: String) extends Action 35 | 36 | class FiddleHandler[M](modelRW: ModelRW[M, FiddleData], fidRW: ModelRW[M, Option[FiddleId]]) extends ActionHandler(modelRW) { 37 | 38 | override def handle = { 39 | case SelectLibrary(lib) => 40 | updated(value.copy(libraries = value.libraries :+ lib)) 41 | 42 | case DeselectLibrary(lib) => 43 | updated(value.copy(libraries = value.libraries.filterNot(_ == lib))) 44 | 45 | case SelectScalaVersion(version) => 46 | updated(value.copy(scalaVersion = version)) 47 | 48 | case SelectScalaJSVersion(version) => 49 | updated(value.copy(scalaJSVersion = version)) 50 | 51 | case UpdateInfo(name, description) => 52 | updated(value.copy(name = name, description = description)) 53 | 54 | case UpdateSource(source) => 55 | updated(value.copy(sourceCode = source, modified = System.currentTimeMillis())) 56 | 57 | case SaveFiddle(source) => 58 | val newFiddle = value.copy(sourceCode = source, available = Seq.empty) 59 | val saveF: Future[Action] = AjaxClient[Api].save(newFiddle).call().map { 60 | case Right(fid) => 61 | UpdateId(fid) 62 | case Left(error) => 63 | NoAction 64 | } 65 | updated(value.copy(sourceCode = source), Effect(saveF)) 66 | 67 | case UpdateFiddle(source) => 68 | if (fidRW().isDefined) { 69 | val newFiddle = value.copy(sourceCode = source, available = Seq.empty) 70 | val updateF: Future[Action] = AjaxClient[Api].update(newFiddle, fidRW().get.id).call().map { 71 | case Right(fid) => 72 | UpdateId(fid) 73 | case Left(error) => 74 | NoAction 75 | } 76 | updated(value.copy(sourceCode = source), Effect(updateF)) 77 | } else { 78 | noChange 79 | } 80 | 81 | case LoadFiddle(url) => 82 | val loadF = dom.ext.Ajax.get(url).map(r => UpdateSource(r.responseText)) 83 | effectOnly(Effect(loadF)) 84 | 85 | case ForkFiddle(source) => 86 | if (fidRW().isDefined) { 87 | val newFiddle = value.copy(sourceCode = source, available = Seq.empty) 88 | val forkF: Future[Action] = AjaxClient[Api].fork(newFiddle, fidRW().get.id, fidRW().get.version).call().map { 89 | case Right(fid) => 90 | UpdateId(fid) 91 | case Left(error) => 92 | NoAction 93 | } 94 | updated(value.copy(sourceCode = source), Effect(forkF)) 95 | } else { 96 | noChange 97 | } 98 | 99 | case UpdateId(fid, silent) => 100 | if (silent) 101 | ModelUpdate(fidRW.updated(Some(fid))) 102 | else 103 | ModelUpdateEffect(fidRW.updated(Some(fid)), Effect.action(NavigateTo(AppRouter.EditorPage(fid.id, fid.version)))) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /client/src/main/scala/scalafiddle/client/LoginHandler.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.client 2 | 3 | import autowire._ 4 | import diode._ 5 | import diode.data.Ready 6 | 7 | import scala.concurrent.ExecutionContext.Implicits.global 8 | import scalafiddle.shared._ 9 | 10 | case object UpdateLoginInfo extends Action 11 | 12 | case class UpdateLoginProviders(loginProviders: Seq[LoginProvider]) extends Action 13 | 14 | case class UpdateUserInfo(userInfo: UserInfo) extends Action 15 | 16 | class LoginHandler[M](modelRW: ModelRW[M, LoginData]) extends ActionHandler(modelRW) { 17 | override def handle = { 18 | case UpdateLoginInfo => 19 | val loginProviders = Effect(AjaxClient[Api].loginProviders().call().map(UpdateLoginProviders)) 20 | val userInfo = Effect(AjaxClient[Api].userInfo().call().map(UpdateUserInfo)) 21 | effectOnly(loginProviders + userInfo) 22 | 23 | case UpdateLoginProviders(loginProviders) => 24 | updated(value.copy(loginProviders = Ready(loginProviders))) 25 | 26 | case UpdateUserInfo(userInfo) => 27 | updated(value.copy(userInfo = Ready(userInfo))) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/src/main/scala/scalafiddle/client/Utils.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.client 2 | 3 | import org.scalajs.dom 4 | 5 | import scala.language.implicitConversions 6 | import scala.scalajs.js 7 | import scala.scalajs.js.JSConverters._ 8 | import scala.scalajs.js.annotation.JSGlobal 9 | import scala.scalajs.js.typedarray.Uint8Array 10 | import scala.scalajs.js.| 11 | 12 | class JsVal(val value: js.Dynamic) { 13 | def get(name: String): Option[JsVal] = { 14 | (value.selectDynamic(name): Any) match { 15 | case () => None 16 | case v => Some(JsVal(v.asInstanceOf[js.Dynamic])) 17 | } 18 | } 19 | 20 | def apply(name: String): JsVal = get(name).get 21 | def apply(index: Int): JsVal = value.asInstanceOf[js.Array[JsVal]](index) 22 | 23 | def keys: Seq[String] = js.Object.keys(value.asInstanceOf[js.Object]).toSeq.map(x => x: String) 24 | def values: Seq[JsVal] = keys.map(x => JsVal(value.selectDynamic(x))) 25 | 26 | def isDefined: Boolean = !js.isUndefined(value) 27 | def isNull: Boolean = value eq null 28 | 29 | def asDouble: Double = value.asInstanceOf[Double] 30 | def asBoolean: Boolean = value.asInstanceOf[Boolean] 31 | def asString: String = value.asInstanceOf[String] 32 | 33 | override def toString: String = js.JSON.stringify(value) 34 | } 35 | 36 | object JsVal { 37 | implicit def jsVal2jsAny(v: JsVal): js.Any = v.value 38 | 39 | implicit def jsVal2String(v: JsVal): js.Any = v.toString 40 | 41 | def parse(value: String) = new JsVal(js.JSON.parse(value)) 42 | 43 | def apply(value: js.Any) = new JsVal(value.asInstanceOf[js.Dynamic]) 44 | 45 | def obj(keyValues: (String, js.Any)*) = { 46 | val obj = new js.Object().asInstanceOf[js.Dynamic] 47 | for ((k, v) <- keyValues) { 48 | obj.updateDynamic(k)(v.asInstanceOf[js.Any]) 49 | } 50 | new JsVal(obj) 51 | } 52 | 53 | def arr(values: js.Any*) = { 54 | new JsVal(values.toJSArray.asInstanceOf[js.Dynamic]) 55 | } 56 | } 57 | 58 | @JSGlobal("Zlib.Gzip") 59 | @js.native 60 | class Gzip(data: js.Array[Byte]) extends js.Object { 61 | def compress(): Uint8Array = js.native 62 | } 63 | 64 | @JSGlobal("Zlib.Gunzip") 65 | @js.native 66 | class Gunzip(data: js.Array[Byte]) extends js.Object { 67 | def decompress(): Uint8Array = js.native 68 | } 69 | 70 | @JSGlobal("sha1") 71 | @js.native 72 | object SHA1 extends js.Object { 73 | def apply(s: String): String = js.native 74 | } 75 | 76 | @js.native 77 | @JSGlobal("ScalaFiddleConfig") 78 | object ScalaFiddleConfig extends js.Object { 79 | val compilerURL: String = js.native 80 | val helpURL: String = js.native 81 | val scalaVersions: js.Array[String] = js.native 82 | val scalaJSVersions: js.Array[String] = js.native 83 | } 84 | 85 | @js.native 86 | @JSGlobal 87 | object Mousetrap extends js.Object { 88 | def bind(key: String | js.Array[String], f: js.Function1[dom.KeyboardEvent, Boolean], event: String = js.native): Unit = 89 | js.native 90 | 91 | def bindGlobal( 92 | key: String | js.Array[String], 93 | f: js.Function1[dom.KeyboardEvent, Boolean], 94 | event: String = js.native 95 | ): Unit = js.native 96 | 97 | def unbind(key: String): Unit = js.native 98 | 99 | def reset(): Unit = js.native 100 | } 101 | -------------------------------------------------------------------------------- /client/src/main/scala/scalafiddle/client/component/Dropdown.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.client.component 2 | 3 | import japgolly.scalajs.react._ 4 | import org.scalajs.dom 5 | import org.scalajs.dom.raw.{HTMLElement, MouseEvent} 6 | import scalajs.js 7 | 8 | object Dropdown { 9 | import japgolly.scalajs.react.vdom.all._ 10 | 11 | case class State(isOpen: Boolean = false) 12 | 13 | case class Props(classes: String, buttonContent: VdomNode, content: (() => Callback) => VdomNode) 14 | 15 | case class Backend($ : BackendScope[Props, State]) { 16 | def render(props: Props, state: State) = { 17 | div(cls := s"ui dropdown ${props.classes} ${if (state.isOpen) "active visible" else ""}", onClick ==> { 18 | (e: ReactEventFromHtml) => 19 | dropdownClicked(e, state.isOpen) 20 | })( 21 | props.buttonContent, 22 | props.content(() => closeDropdown()).when(state.isOpen) 23 | ) 24 | } 25 | 26 | val closeFn: js.Function1[MouseEvent, _] = (e: MouseEvent) => closeDropdown(e) 27 | 28 | def dropdownClicked(e: ReactEventFromHtml, isOpen: Boolean): Callback = { 29 | if (!isOpen) { 30 | Callback { 31 | dom.document.addEventListener("click", closeFn) 32 | } >> $.modState(s => s.copy(isOpen = true)) 33 | } else { 34 | Callback.empty 35 | } 36 | } 37 | 38 | def closeDropdown(e: MouseEvent): Unit = { 39 | val state = $.state.runNow() 40 | val node = $.getDOMNode.runNow().asInstanceOf[HTMLElement] 41 | if (state.isOpen && !node.contains(e.target.asInstanceOf[HTMLElement])) { 42 | closeDropdown().runNow() 43 | } 44 | } 45 | 46 | def closeDropdown(): Callback = { 47 | dom.document.removeEventListener("click", closeFn) 48 | $.modState(s => s.copy(isOpen = false)) 49 | } 50 | } 51 | 52 | val component = ScalaComponent 53 | .builder[Props]("FiddleEditor") 54 | .initialState(State()) 55 | .renderBackend[Backend] 56 | .build 57 | 58 | def apply(classes: String, buttonContent: VdomNode)(content: (() => Callback) => VdomNode) = 59 | component(Props(classes, buttonContent, content)) 60 | } 61 | -------------------------------------------------------------------------------- /client/src/main/scala/scalafiddle/client/component/EmbedEditor.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.client.component 2 | 3 | import japgolly.scalajs.react._ 4 | import org.scalajs.dom.raw.HTMLTextAreaElement 5 | 6 | import scalafiddle.client.ScalaFiddleConfig 7 | import scalafiddle.shared.FiddleId 8 | import scala.scalajs.js 9 | 10 | object EmbedEditor { 11 | import japgolly.scalajs.react.vdom.Implicits._ 12 | 13 | sealed trait Layout { 14 | def split: Int 15 | def updated(split: Int): Layout 16 | } 17 | 18 | case class Horizontal(split: Int) extends Layout { 19 | override def updated(split: Int): Layout = Horizontal(split) 20 | } 21 | 22 | case class Vertical(split: Int) extends Layout { 23 | override def updated(split: Int): Layout = Vertical(split) 24 | } 25 | 26 | val themes = Seq("light", "dark") 27 | 28 | case class State(theme: String, layout: Layout) 29 | 30 | case class Props(fiddleId: FiddleId) 31 | 32 | case class Backend($ : BackendScope[Props, State]) { 33 | def render(props: Props, state: State) = { 34 | import japgolly.scalajs.react.vdom.all._ 35 | 36 | def createIframeSource: String = { 37 | val src = new StringBuilder(s"${ScalaFiddleConfig.compilerURL}/embed?sfid=${props.fiddleId}") 38 | state.theme match { 39 | case "light" => 40 | case theme => src.append(s"&theme=$theme") 41 | } 42 | state.layout match { 43 | case Horizontal(50) => 44 | case Horizontal(split) => src.append(s"&layout=h$split") 45 | case Vertical(split) => src.append(s"&layout=v$split") 46 | } 47 | src.toString() 48 | } 49 | 50 | def createEmbedCode: String = { 51 | s"""""" 52 | } 53 | 54 | div(cls := "embed-editor")( 55 | div(cls := "options")( 56 | div(cls := "ui form")( 57 | div(cls := "header", "Theme"), 58 | div(cls := "grouped fields")( 59 | themes.toTagMod { theme => 60 | div(cls := "field")( 61 | div(cls := "ui radio checkbox")( 62 | input.radio(name := "theme", checked := (state.theme == theme), onChange --> themeChanged(theme)), 63 | label(theme) 64 | ) 65 | ) 66 | } 67 | ), 68 | div(cls := "header", "Layout"), 69 | div(cls := "two fields")( 70 | div(cls := "grouped fields")( 71 | label(`for` := "layout")("Direction"), 72 | div(cls := "field")( 73 | div(cls := "ui radio checkbox")( 74 | input.radio( 75 | name := "layout", 76 | checked := state.layout.isInstanceOf[Horizontal], 77 | onChange --> layoutChanged(Horizontal(state.layout.split)) 78 | ), 79 | label("Horizontal") 80 | ) 81 | ), 82 | div(cls := "field")( 83 | div(cls := "ui radio checkbox")( 84 | input.radio( 85 | name := "layout", 86 | checked := state.layout.isInstanceOf[Vertical], 87 | onChange --> layoutChanged(Vertical(state.layout.split)) 88 | ), 89 | label("Vertical") 90 | ) 91 | ) 92 | ), 93 | div(cls := "field")( 94 | label(`for` := "split")("Split"), 95 | input.range(min := 15, max := 85, value := state.layout.split, onChange ==> splitChanged) 96 | ) 97 | ), 98 | div(cls := "field")( 99 | div(cls := "header", "Embed code"), 100 | textarea( 101 | cls := "embed-code", 102 | value := createEmbedCode, 103 | readOnly := true, 104 | onClick ==> { (e: ReactEventFromHtml) => 105 | e.target.focus(); e.target.asInstanceOf[HTMLTextAreaElement].select(); Callback.empty 106 | } 107 | ) 108 | ) 109 | ) 110 | ), 111 | div(cls := "preview")( 112 | div(cls := "header", "Preview"), 113 | iframe( 114 | height := "400px", 115 | width := "100%", 116 | frameBorder := 0, 117 | style := js.Dictionary("width" -> "100%", "overflow" -> "hidden"), 118 | src := s"$createIframeSource&preview=true" 119 | ) 120 | ) 121 | ) 122 | } 123 | 124 | def themeChanged(theme: String): Callback = { 125 | $.modState(_.copy(theme = theme)) 126 | } 127 | 128 | def layoutChanged(layout: Layout): Callback = { 129 | $.modState(_.copy(layout = layout)) 130 | } 131 | 132 | def splitChanged(e: ReactEventFromInput): Callback = { 133 | val split = e.target.value.toInt 134 | $.modState(s => s.copy(layout = s.layout.updated(split = split))) 135 | } 136 | 137 | } 138 | 139 | val component = ScalaComponent 140 | .builder[Props]("EmbedEditor") 141 | .initialState(State("light", Horizontal(50))) 142 | .renderBackend[Backend] 143 | .build 144 | 145 | def apply(fiddleId: FiddleId) = component(Props(fiddleId)) 146 | } 147 | -------------------------------------------------------------------------------- /client/src/main/scala/scalafiddle/client/component/JQuery.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.client.component 2 | 3 | import org.scalajs.dom._ 4 | 5 | import scala.scalajs.js 6 | import scala.scalajs.js.annotation.JSGlobal 7 | 8 | /** 9 | * Minimal facade for JQuery. Use https://github.com/scala-js/scala-js-jquery or 10 | * https://github.com/jducoeur/jquery-facade for more complete one. 11 | */ 12 | @js.native 13 | trait JQueryEventObject extends Event { 14 | var data: js.Any = js.native 15 | } 16 | 17 | @js.native 18 | @JSGlobal("jQuery") 19 | object JQueryStatic extends js.Object { 20 | def apply(selector: js.Any): JQuery = js.native 21 | } 22 | 23 | @js.native 24 | trait JQuery extends js.Object {} 25 | -------------------------------------------------------------------------------- /client/src/main/scala/scalafiddle/client/component/SemanticUI.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.client.component 2 | 3 | import scala.language.implicitConversions 4 | import scala.scalajs.js 5 | 6 | object SemanticUI { 7 | @js.native 8 | trait SemanticJQuery extends JQuery { 9 | def accordion(params: js.Any*): SemanticJQuery = js.native 10 | def dropdown(params: js.Any*): SemanticJQuery = js.native 11 | def checkbox(): SemanticJQuery = js.native 12 | } 13 | 14 | implicit def jq2bootstrap(jq: JQuery): SemanticJQuery = jq.asInstanceOf[SemanticJQuery] 15 | } 16 | -------------------------------------------------------------------------------- /client/src/main/scala/scalafiddle/client/component/Sidebar.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.client.component 2 | 3 | import diode.Action 4 | import diode.react.ModelProxy 5 | import japgolly.scalajs.react._ 6 | import org.scalajs.dom.raw.HTMLDivElement 7 | 8 | import scala.scalajs.js 9 | import scalafiddle.client._ 10 | import scalafiddle.shared._ 11 | 12 | object Sidebar { 13 | import japgolly.scalajs.react.vdom.all._ 14 | 15 | import SemanticUI._ 16 | 17 | private var sideBarRef: HTMLDivElement = _ 18 | private var accordionRef: HTMLDivElement = _ 19 | 20 | sealed trait LibMode 21 | 22 | case object SelectedLib extends LibMode 23 | 24 | case object AvailableLib extends LibMode 25 | 26 | case class Props(data: ModelProxy[FiddleData]) { 27 | def dispatch(a: Action) = data.dispatchCB(a) 28 | } 29 | 30 | case class State(showAllVersions: Boolean, isOpen: Boolean = true) 31 | 32 | case class Backend($ : BackendScope[Props, State]) { 33 | 34 | def render(props: Props, state: State) = { 35 | val fd = props.data() 36 | // filter the list of available libs 37 | val availableVersions = fd.available 38 | .filterNot(lib => fd.libraries.exists(_.name == lib.name)) 39 | .filter(lib => lib.scalaVersions.contains(fd.scalaVersion)) 40 | .filter(lib => lib.scalaJSVersions.contains(fd.scalaJSVersion)) 41 | 42 | // hide alternative versions, if requested 43 | val available = 44 | if (state.showAllVersions) 45 | availableVersions 46 | else 47 | availableVersions.foldLeft(Vector.empty[Library]) { 48 | case (libs, lib) => if (libs.exists(l => l.name == lib.name)) libs else libs :+ lib 49 | } 50 | val libGroups = available.groupBy(_.group).toSeq.sortBy(_._1).map(group => (group._1, group._2.sortBy(_.name))) 51 | 52 | val (authorName, authorImage) = fd.author match { 53 | case Some(userInfo) if userInfo.id != "anonymous" => 54 | (userInfo.name, userInfo.avatarUrl.getOrElse("/assets/images/anon.png")) 55 | case _ => 56 | ("Anonymous", "/assets/images/anon.png") 57 | } 58 | div.ref(sideBarRef = _)(cls := "sidebar")( 59 | div.ref(accordionRef = _)(cls := "ui accordion")( 60 | div(cls := "title large active", "Info", i(cls := "icon dropdown")), 61 | div(cls := "content active")( 62 | div(cls := "ui form")( 63 | div(cls := "field")( 64 | label("Name"), 65 | input.text(placeholder := "Untitled", name := "name", value := fd.name, onChange ==> { 66 | (e: ReactEventFromInput) => 67 | props.dispatch(UpdateInfo(e.target.value, fd.description)) 68 | }) 69 | ), 70 | div(cls := "field")( 71 | label("Description"), 72 | input.text( 73 | placeholder := "Enter description", 74 | name := "description", 75 | value := fd.description, 76 | onChange ==> { (e: ReactEventFromInput) => 77 | props.dispatch(UpdateInfo(fd.name, e.target.value)) 78 | } 79 | ) 80 | ), 81 | div(cls := "field")( 82 | label("Author"), 83 | img(cls := "author", src := authorImage), 84 | authorName 85 | ) 86 | ) 87 | ), 88 | div(cls := "title", "Libraries", i(cls := "icon dropdown")), 89 | div(cls := "content")( 90 | div( 91 | cls := "ui horizontal divider header", 92 | (style := js.Dynamic.literal(display = "none")).when(fd.libraries.isEmpty), 93 | "Selected" 94 | ), 95 | div(cls := "liblist", (style := js.Dynamic.literal(display = "none")).when(fd.libraries.isEmpty))( 96 | div(cls := "ui middle aligned divided list")( 97 | fd.libraries.toTagMod(renderLibrary(_, SelectedLib, fd.scalaVersion, props.dispatch)) 98 | ) 99 | ), 100 | div(cls := "ui horizontal divider header", "Available"), 101 | div(cls := "ui checkbox")( 102 | input.checkbox( 103 | name := "all-versions", 104 | checked := state.showAllVersions, 105 | onChange --> $.modState(s => s.copy(showAllVersions = !s.showAllVersions)) 106 | ), 107 | label("Show all versions") 108 | ), 109 | div(cls := "liblist")( 110 | libGroups.toTagMod { 111 | case (group, libraries) => 112 | div( 113 | h5(cls := "lib-group", group.replaceFirst("\\d+:", "")), 114 | div(cls := "ui middle aligned divided list")( 115 | libraries.toTagMod(renderLibrary(_, AvailableLib, fd.scalaVersion, props.dispatch)) 116 | ) 117 | ) 118 | } 119 | ), 120 | div(cls := "ui grid")( 121 | div(cls := "eight wide column")( 122 | h4("Scala version") 123 | ), 124 | div(cls := "eight wide column right aligned")( 125 | div(cls := "grouped fields")( 126 | ScalaFiddleConfig.scalaVersions.toTagMod { version => 127 | div(cls := "field")( 128 | div(cls := "ui radio checkbox")( 129 | input.radio( 130 | name := "scalaversion", 131 | value := version, 132 | checked := props.data().scalaVersion == version, 133 | onChange ==> { (e: ReactEventFromInput) => 134 | val newVersion = e.currentTarget.value 135 | props.dispatch(SelectScalaVersion(newVersion)) 136 | } 137 | ), 138 | label(version) 139 | ) 140 | ) 141 | } 142 | ) 143 | ) 144 | ), 145 | div(cls := "ui grid")( 146 | div(cls := "eight wide column")( 147 | h4("Scala.js version") 148 | ), 149 | div(cls := "eight wide column right aligned")( 150 | div(cls := "grouped fields")( 151 | ScalaFiddleConfig.scalaJSVersions.toTagMod { version => 152 | div(cls := "field")( 153 | div(cls := "ui radio checkbox")( 154 | input.radio( 155 | name := "scalajsversion", 156 | value := version, 157 | checked := props.data().scalaJSVersion == version, 158 | onChange ==> { (e: ReactEventFromInput) => 159 | val newVersion = e.currentTarget.value 160 | props.dispatch(SelectScalaJSVersion(newVersion)) 161 | } 162 | ), 163 | label(version + ".x") 164 | ) 165 | ) 166 | } 167 | ) 168 | ) 169 | ) 170 | ) 171 | ), 172 | div(cls := "bottom")( 173 | div( 174 | cls := "ui icon basic button toggle", 175 | onClick ==> { (e: ReactEventFromHtml) => 176 | Callback { 177 | sideBarRef.classList.toggle("folded") 178 | } >> $.modState(s => s.copy(isOpen = !state.isOpen)) 179 | } 180 | )(if (state.isOpen) Icon.angleDoubleLeft else Icon.angleDoubleRight) 181 | ) 182 | ) 183 | } 184 | 185 | def renderLibrary(lib: Library, mode: LibMode, scalaVersion: String, dispatch: Action => Callback) = { 186 | val (action, icon) = mode match { 187 | case SelectedLib => (DeselectLibrary(lib), i(cls := "remove red icon")) 188 | case AvailableLib => (SelectLibrary(lib), i(cls := "plus green icon")) 189 | } 190 | div(cls := "item")( 191 | div(cls := "right floated")( 192 | lib.exampleUrl 193 | .map(url => 194 | button( 195 | cls := s"mini ui icon basic button", 196 | title := s"Load a sample fiddle for ${lib.name}", 197 | onClick --> dispatch(LoadFiddle(url)) 198 | )(i(cls := "file code outline icon codeicon")) 199 | ) 200 | .whenDefined 201 | .when(mode == SelectedLib), 202 | button(cls := s"mini ui icon basic button", onClick --> dispatch(action))(icon) 203 | ), 204 | a(href := lib.docUrl, target := "_blank")( 205 | div(cls := "content left floated")( 206 | b(lib.name), 207 | " ", 208 | span(cls := (if (lib.scalaVersions.contains(scalaVersion)) "text grey" else "text red"), lib.version) 209 | ) 210 | ) 211 | ) 212 | } 213 | } 214 | 215 | val component = ScalaComponent 216 | .builder[Props]("Sidebar") 217 | .initialState(State(showAllVersions = false)) 218 | .renderBackend[Backend] 219 | .componentDidMount(_ => 220 | Callback { 221 | JQueryStatic(accordionRef).accordion(js.Dynamic.literal(exclusive = true, animateChildren = false)) 222 | } 223 | ) 224 | .build 225 | 226 | def apply(data: ModelProxy[FiddleData]) = component(Props(data)) 227 | } 228 | -------------------------------------------------------------------------------- /client/src/main/scala/scalafiddle/client/component/UserLogin.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.client.component 2 | 3 | import diode.ModelR 4 | import diode.data.Ready 5 | import japgolly.scalajs.react.vdom.all._ 6 | import japgolly.scalajs.react.{BackendScope, _} 7 | 8 | import scalafiddle.client.{AppCircuit, AppModel, LoadUserFiddles, LoginData} 9 | 10 | object UserLogin { 11 | 12 | case class Props(loginData: ModelR[AppModel, LoginData]) 13 | 14 | case class Backend($ : BackendScope[Props, Unit]) { 15 | var unsubscribe: () => Unit = () => () 16 | 17 | def render(props: Props): VdomElement = { 18 | val loginData = props.loginData() 19 | loginData.userInfo match { 20 | case Ready(userInfo) if userInfo.loggedIn => 21 | div(cls := "userinfo")( 22 | img(cls := "author", src := userInfo.avatarUrl.getOrElse("/assets/images/anon.png")), 23 | Dropdown("top right pointing", div(cls := "username", userInfo.name))(closeCB => 24 | div(cls := "ui vertical menu", display.block)( 25 | a(cls := "item", onClick --> { Callback(AppCircuit.dispatch(LoadUserFiddles)) >> closeCB() })("My fiddles") 26 | ) 27 | ), 28 | a(href := "/signout", div(cls := "ui basic button", "Sign out")) 29 | ) 30 | case Ready(userInfo) if !userInfo.loggedIn => 31 | loginData.loginProviders match { 32 | case Ready(loginProviders) if loginProviders.size == 1 => 33 | val provider = loginProviders.head 34 | a(href := s"/authenticate/${provider.id}")( 35 | div(cls := "ui button login")( 36 | img(src := provider.logoUrl), 37 | s"Sign in with ${provider.name}" 38 | ) 39 | ) 40 | case Ready(loginProviders) => 41 | Dropdown("top basic button embed-options", span("Sign in", Icon.caretDown))(_ => 42 | div(cls := "menu", display.block)(div("Login providers")) 43 | ) 44 | case _ => 45 | div(img(src := "/assets/images/wait-ring.gif")) 46 | } 47 | case _ => 48 | div(img(src := "/assets/images/wait-ring.gif")) 49 | } 50 | } 51 | 52 | def mounted(props: Props): Callback = { 53 | Callback { 54 | // subscribe to changes in compiler data 55 | unsubscribe = AppCircuit.subscribe(props.loginData)(_ => $.forceUpdate.runNow()) 56 | } 57 | } 58 | 59 | def unmounted: Callback = { 60 | Callback(unsubscribe()) 61 | } 62 | } 63 | 64 | val component = ScalaComponent 65 | .builder[Props]("UserLogin") 66 | .renderBackend[Backend] 67 | .componentDidMount(scope => scope.backend.mounted(scope.props)) 68 | .componentWillUnmount(scope => scope.backend.unmounted) 69 | .build 70 | 71 | def apply(loginInfo: ModelR[AppModel, LoginData]) = component(Props(loginInfo)) 72 | } 73 | -------------------------------------------------------------------------------- /client/src/test/scala/scalafiddle/client/DummySpec.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.client 2 | 3 | import org.scalatest._ 4 | 5 | class DummySpec extends WordSpec {} 6 | -------------------------------------------------------------------------------- /docs/Welcome-src.md: -------------------------------------------------------------------------------- 1 | # Welcome to ScalaFiddle 2 | 3 | ScalaFiddle is an online playground for creating, sharing and embedding Scala fiddles (little Scala programs that run 4 | directly in your browser). 5 | 6 | ## Quick start 7 | 8 | Just write some Scala code in the editor on the left and click the **Run** button (or press `Ctrl-Enter` on Windows/Linux, or 9 | `Cmd-Enter` on macos) to compile and run your code. The result will be shown here, replacing this documentation. You can 10 | always get this page back by clicking the **Help** button. 11 | 12 | A truly simple example to get you started: 13 | 14 | ```scala 15 | println("Hello world!") 16 | ``` 17 | 18 | ## Saving your masterpiece 19 | 20 | To make the fruit of your labors immortal, click the **Save** button. This will assign a random identifier to your fiddle and 21 | update the page URL to something like `https://scalafiddle.io/sf/S0mXhH9/0`. The last part of the URL is a _version number_, 22 | which is incremented every time you updated the fiddle. This means that once a fiddle is saved, it cannot be modified, but a 23 | new version can be created. 24 | 25 | By default, all fiddles are _anonymous_, but if you want to keep them under your own control, you should create an account 26 | via **Sign in with GitHub**. This way no one else but you can update the saved fiddle. Others can only **Fork** it to 27 | continue work under their own account. 28 | 29 | To make sure you can later find your fiddle, remember to add a **Name** and **Description** to your fiddle before saving. 30 | 31 | ## Using libraries 32 | 33 | You can experiment with different supported Scala libraries by selecting them from the **Libraries** section on the left 34 | sidebar. Just press the **+** button to add a library to your fiddle, and **X** to remove it. The latest version is shown by 35 | default, but you can also **Show all versions** if you need to work with an older one. 36 | 37 | ![libraries](libraries.png) 38 | 39 | By default new fiddles use Scala 2.12, but you can also select another Scala version. 40 | 41 | ## Editing code 42 | 43 | Although the editor in ScalaFiddle is no match for a full IDE, it's quite adequate for small fiddles. It provides rudimentary 44 | syntax highlighting and things like _search_ (`Ctrl/Cmd-F`). You can also access code completion by pressing `Ctrl/Cmd-Space` 45 | to get a list of potential completions suitable for that location. Note that this feature actually calls the remote compiler 46 | to perform the code completion, so there might be a slight delay. 47 | 48 | Your fiddle code is actually contained in a template, which you can show by clicking the **SCALA** button in the top-right 49 | corner of the editor. 50 | 51 | ![showtemplate](showtemplate.png) 52 | 53 | This template makes sure your code is contained in a single _object_ that is exposed to JavaScript so that it can be 54 | executed. Normally you don't need to deal with the template, but sometimes you need to take things outside this object and 55 | then it's necessary to edit the code on the outside. 56 | 57 | ## Special features 58 | 59 | Because ScalaFiddle runs in the browser, you can access browser features like the DOM and Canvas in your fiddle. There is a 60 | helper object called `Fiddle` that contains methods for accessing the DOM and the built-in canvas. 61 | 62 | ### Using the DOM 63 | 64 | The simplest way for using the DOM is to generate your DOM elements using the included **Scalatags** library. For this you 65 | need to `import scalatags.JsDom.all._` and use `Fiddle.print` to show the DOM, as in the example below. 66 | 67 | ```scala 68 | import scalatags.JsDom.all._ 69 | 70 | val h = h1("Hello world").render 71 | Fiddle.print(h) 72 | ``` 73 | 74 | You can also make the DOM interactive as demonstrated by the simple fiddle below: 75 | 76 | ```scala 77 | import scalatags.JsDom.all._ 78 | 79 | val textInput = input(placeholder := "write text here").render 80 | val lengthButton = button("Length").render 81 | val result = p.render 82 | lengthButton.onclick = (e: Any) => result.innerHTML = textInput.value.length.toString 83 | 84 | Fiddle.print(div(textInput, lengthButton), result) 85 | ``` 86 | 87 | ### Drawing to canvas 88 | 89 | ScalaFiddle automatically provides a rectangular canvas in the output panel that you can use for drawing. The canvas drawing 90 | context is accessible via `Fiddle.draw` and represents a 91 | [`CanvasRenderingContext2D`](https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D). 92 | 93 | To draw a small 50 by 50 pixel rectangle at position 10,10: 94 | ```scala 95 | Fiddle.draw.rect(10, 10, 50, 50) 96 | Fiddle.draw.stroke 97 | ``` 98 | 99 | For a more complex example, check out the Hilbert curve demonstration 100 | 101 | ## Embedding 102 | 103 | While editing and sharing fiddles on scalafiddle.io website is great, you can also embed your fiddles to any web page you 104 | like. The embedded fiddle provides interactive editing, so you could for example include a short example on your 105 | documentation site or in your blog and have viewers play with it right there, on your own page. 106 | 107 | To create an embedded fiddle, just click the **Embed** button, which will present you with some options, the HTML code you 108 | need to copy paste, and a preview of what the embedded fiddle will look like. 109 | 110 | ![embed](embed.png) 111 | 112 | Embedding instructive fiddles to your library documentation is a great way to let your library users try out features without 113 | installing anything. 114 | 115 | ## Integration to documentation 116 | 117 | ScalaFiddle can also be integrated directly into documentation without embedding from the editor. See the section on 118 | [ScalaFiddle integration](https://github.com/scalafiddle/scalafiddle-core/blob/master/integrations/README.md) for more information. 119 | 120 | ## Source 121 | 122 | ScalaFiddle is open source and you can find all the source code [in GitHub repositories](https://github.com/scalafiddle) -------------------------------------------------------------------------------- /docs/Welcome.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | scalafiddle-editor 14 | 15 | 16 | 17 | 18 | 19 | 31 | 32 | 33 |
34 |
35 |

Welcome to ScalaFiddle

36 | 37 |

ScalaFiddle is an online playground for creating, sharing and embedding Scala fiddles (little Scala programs that 38 | run 39 | directly in your browser).

40 | 41 |

Quick start

42 | 43 |

Just write some Scala code in the editor on the left and click the Run button (or press Ctrl-Enter on Windows/Linux, or 45 | Cmd-Enter on macos) to compile and run your code. The result will be shown 46 | here, replacing this documentation. You can 47 | always get this page back by clicking the Help button.

48 | 49 |

A truly simple example to get you started:

50 | 51 |
52 |
println("Hello world!")
 54 | 
55 |
56 |
57 | 58 |

Saving your masterpiece

59 | 60 |

To make the fruit of your labors immortal, click the Save button. This will assign a random 61 | identifier to your fiddle and 62 | update the page URL to something like https://scalafiddle.io/sf/S0mXhH9/0. 63 | The last part of the URL is a version number, 64 | which is incremented every time you updated the fiddle. This means that once a fiddle is saved, it cannot be 65 | modified, but a 66 | new version can be created.

67 | 68 |

By default, all fiddles are anonymous, but if you want to keep them under your own control, you should 69 | create an account 70 | via Sign in with GitHub. This way no one else but you can update the saved fiddle. Others can 71 | only Fork it to 72 | continue work under their own account.

73 | 74 |

To make sure you can later find your fiddle, remember to add a Name and 75 | Description to your fiddle before saving.

76 | 77 |

Using libraries

78 | 79 |

You can experiment with different supported Scala libraries by selecting them from the Libraries 80 | section on the left 81 | sidebar. Just press the + button to add a library to your fiddle, and X to 82 | remove it. The latest version is shown by 83 | default, but you can also Show all versions if you need to work with an older one.

84 | 85 |

libraries

86 | 87 |

By default new fiddles use Scala 2.12, but you can also select another Scala version.

88 | 89 |

Editing code

90 | 91 |

Although the editor in ScalaFiddle is no match for a full IDE, it’s quite adequate for small fiddles. It provides 92 | rudimentary 93 | syntax highlighting and things like search (Ctrl/Cmd-F). You can 94 | also access code completion by pressing Ctrl/Cmd-Space 95 | to get a list of potential completions suitable for that location. Note that this feature actually calls the 96 | remote compiler 97 | to perform the code completion, so there might be a slight delay.

98 | 99 |

Your fiddle code is actually contained in a template, which you can show by clicking the SCALA 100 | button in the top-right 101 | corner of the editor.

102 | 103 |

showtemplate

104 | 105 |

This template makes sure your code is contained in a single object that is exposed to JavaScript so that 106 | it can be 107 | executed. Normally you don’t need to deal with the template, but sometimes you need to take things outside this 108 | object and 109 | then it’s necessary to edit the code on the outside.

110 | 111 |

Special features

112 | 113 |

Because ScalaFiddle runs in the browser, you can access browser features like the DOM and Canvas in your fiddle. 114 | There is a 115 | helper object called Fiddle that contains methods for accessing the DOM 116 | and the built-in canvas.

117 | 118 |

Using the DOM

119 | 120 |

The simplest way for using the DOM is to generate your DOM elements using the included Scalatags 121 | library. For this you 122 | need to import scalatags.JsDom.all._ and use Fiddle.print to show the DOM, as in the example below.

124 | 125 |
126 |
import scalatags.JsDom.all._
127 | 
128 | val h = h1("Hello world").render
131 | Fiddle.print(h)
133 | 
134 |
135 |
136 | 137 |

You can also make the DOM interactive as demonstrated by the simple fiddle below:

138 | 139 |
140 |
import scalatags.JsDom.all._
141 | 
142 | val textInput = input(placeholder := "write text here").render
145 | val lengthButton = button("Length").render
148 | val result = p.render
150 | lengthButton.onclick = (e: Any) => result.innerHTML = textInput.value.length.toString
156 | 
157 | Fiddle.print(div(textInput, lengthButton), result)
161 | 
162 |
163 |
164 | 165 |

Drawing to canvas

166 | 167 |

ScalaFiddle automatically provides a rectangular canvas in the output panel that you can use for drawing. The 168 | canvas drawing 169 | context is accessible via Fiddle.draw and represents a 170 | CanvasRenderingContext2D 171 |

172 | 173 |

To draw a small 50 by 50 pixel rectangle at position 10,10:

174 |
175 |
Fiddle.draw.rect(10, 10, 50, 50)
179 | Fiddle.draw.stroke
180 | 
181 |
182 |
183 | 184 |

For a more complex example, check out the Hilbert 185 | curve demonstration

186 | 187 |

Embedding

188 | 189 |

While editing and sharing fiddles on scalafiddle.io website is great, you can also embed your fiddles to any web 190 | page you 191 | like. The embedded fiddle provides interactive editing, so you could for example include a short example on your 192 | documentation site or in your blog and have viewers play with it right there, on your own page.

193 | 194 |

To create an embedded fiddle, just click the Embed button, which will present you with some 195 | options, the HTML code you 196 | need to copy paste, and a preview of what the embedded fiddle will look like.

197 | 198 |

embed

199 | 200 |

Embedding instructive fiddles to your library documentation is a great way to let your library users try out 201 | features without 202 | installing anything.

203 | 204 |

Integration to documentation

205 | 206 |

ScalaFiddle can also be integrated directly into documentation without embedding from the editor. See the section 207 | on 208 | ScalaFiddle 209 | integration for more information.

210 | 211 |

Source

212 | 213 |

ScalaFiddle is open source and you can find all the source code in GitHub 215 | repositories.

216 | 217 |
218 |
219 | 220 | 221 | 233 | 234 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {{ site.title | default: site.github.repository_name }} 13 | 14 | 15 | 16 | 17 | 18 | 35 | 36 | 37 |
38 |
39 | {{ content }} 40 |
41 |
42 | 43 | 44 | 65 | 66 | -------------------------------------------------------------------------------- /docs/assets/css/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "{{ site.theme }}"; 5 | 6 | #main_content_wrap { 7 | background: #fff; 8 | border: none; 9 | } 10 | 11 | body { 12 | background: #fff; 13 | font-family: Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif; 14 | } 15 | 16 | .inner { 17 | max-width: 800px; 18 | } 19 | 20 | img { 21 | max-width: 700px; 22 | } 23 | 24 | #main_content pre { 25 | padding: 0.8rem; 26 | margin-top: 0; 27 | margin-bottom: 1rem; 28 | font: 1rem Consolas, "Liberation Mono", Menlo, Courier, monospace; 29 | color: #567482; 30 | word-wrap: normal; 31 | background-color: #f3f6fa; 32 | border: solid 1px #dce6f0; 33 | border-radius: 0.3rem; 34 | max-width: 700px; 35 | } 36 | 37 | #main_content code { 38 | padding: 2px 4px; 39 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; 40 | font-size: 0.9rem; 41 | color: #567482; 42 | background-color: #f3f6fa; 43 | border-radius: 0.3rem; 44 | } 45 | 46 | .highlight pre { 47 | padding: 0.8rem; 48 | overflow: auto; 49 | font-size: 0.9rem; 50 | line-height: 1.45; 51 | border-radius: 0.3rem; 52 | -webkit-overflow-scrolling: touch; 53 | } 54 | 55 | #main_content pre code, #main_content pre tt { 56 | display: inline; 57 | max-width: initial; 58 | padding: 0; 59 | margin: 0; 60 | overflow: initial; 61 | line-height: inherit; 62 | word-wrap: normal; 63 | background-color: transparent; 64 | border: 0; 65 | } 66 | 67 | #main_content pre > code { 68 | padding: 0; 69 | margin: 0; 70 | font-size: 0.9rem; 71 | color: #567482; 72 | word-break: normal; 73 | white-space: pre; 74 | background: transparent; 75 | border: 0; 76 | } 77 | 78 | code { 79 | box-shadow: none; 80 | border: none; 81 | } 82 | -------------------------------------------------------------------------------- /docs/embed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/docs/embed.png -------------------------------------------------------------------------------- /docs/libraries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/docs/libraries.png -------------------------------------------------------------------------------- /docs/showtemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/docs/showtemplate.png -------------------------------------------------------------------------------- /project/Settings.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import org.scalajs.sbtplugin.ScalaJSPlugin 3 | import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ 4 | import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport._ 5 | /** 6 | * Application settings. Configure the build for your application here. 7 | * You normally don't have to touch the actual build definition after this. 8 | */ 9 | object Settings { 10 | 11 | /** The version of your application */ 12 | val version = "1.2.9" 13 | 14 | /** Options for the scala compiler */ 15 | val scalacOptions = Seq( 16 | "-Xlint", 17 | "-unchecked", 18 | "-deprecation", 19 | "-feature" 20 | ) 21 | 22 | /** Declare global dependency versions here to avoid mismatches in multi part dependencies */ 23 | object versions { 24 | val scala = "2.12.10" 25 | val scalatest = "3.0.3" 26 | val scalaDom = "0.9.5" 27 | val scalajsReact = "1.0.0" 28 | val scalaCSS = "0.5.3" 29 | val autowire = "0.2.6" 30 | val booPickle = "1.2.4" 31 | val diode = "1.1.2" 32 | val uTest = "0.4.3" 33 | val upickle = "0.4.4" 34 | val slick = "3.2.0" 35 | val silhouette = "5.0.1" 36 | val kamon = "0.6.7" 37 | val base64 = "0.2.6" 38 | val scalaParse = "0.4.3" 39 | val scalatags = "0.6.7" 40 | 41 | val jQuery = "3.2.0" 42 | val semantic = "2.3.1" 43 | val ace = "1.2.2" 44 | 45 | val react = "15.5.4" 46 | 47 | val scalaJsScripts = "1.1.0" 48 | } 49 | 50 | /** 51 | * These dependencies are shared between JS and JVM projects 52 | * the special %%% function selects the correct version for each project 53 | */ 54 | val sharedDependencies = Def.setting( 55 | Seq( 56 | "com.lihaoyi" %%% "autowire" % versions.autowire, 57 | "com.lihaoyi" %%% "upickle" % versions.upickle, 58 | "org.scalatest" %%% "scalatest" % versions.scalatest % "test" 59 | )) 60 | 61 | /** Dependencies only used by the JVM project */ 62 | val jvmDependencies = Def.setting( 63 | Seq( 64 | "com.typesafe.slick" %% "slick" % versions.slick, 65 | "com.typesafe.slick" %% "slick-hikaricp" % versions.slick, 66 | "ch.qos.logback" % "logback-classic" % "1.2.3", 67 | "net.logstash.logback" % "logstash-logback-encoder" % "4.11", 68 | "com.h2database" % "h2" % "1.4.195", 69 | "org.postgresql" % "postgresql" % "9.4-1206-jdbc41", 70 | "com.mohiva" %% "play-silhouette" % versions.silhouette, 71 | "com.mohiva" %% "play-silhouette-password-bcrypt" % versions.silhouette, 72 | "com.mohiva" %% "play-silhouette-persistence" % versions.silhouette, 73 | "com.mohiva" %% "play-silhouette-crypto-jca" % versions.silhouette, 74 | "net.codingwell" %% "scala-guice" % "4.1.0", 75 | "com.iheart" %% "ficus" % "1.4.1", 76 | "com.github.marklister" %% "base64" % versions.base64, 77 | "com.lihaoyi" %% "scalaparse" % versions.scalaParse, 78 | "com.lihaoyi" %% "scalatags" % versions.scalatags, 79 | "com.vmunier" %% "scalajs-scripts" % versions.scalaJsScripts, 80 | "io.kamon" %% "kamon-core" % versions.kamon, 81 | "io.kamon" %% "kamon-statsd" % versions.kamon, 82 | "org.webjars" % "Semantic-UI" % versions.semantic % Provided 83 | //"org.webjars" % "ace" % versions.ace % Provided 84 | )) 85 | 86 | /** Dependencies only used by the JS project (note the use of %%% instead of %%) */ 87 | val scalajsDependencies = Def.setting( 88 | Seq( 89 | "com.github.japgolly.scalajs-react" %%% "core" % versions.scalajsReact, 90 | "com.github.japgolly.scalajs-react" %%% "extra" % versions.scalajsReact, 91 | "com.olvind" %%% "scalajs-react-components" % "1.0.0-M2", 92 | "com.github.japgolly.scalacss" %%% "ext-react" % versions.scalaCSS, 93 | "io.suzaku" %%% "diode" % versions.diode, 94 | "io.suzaku" %%% "diode-react" % versions.diode, 95 | "com.github.marklister" %%% "base64" % versions.base64, 96 | "org.scala-js" %%% "scalajs-dom" % versions.scalaDom 97 | )) 98 | 99 | /** Dependencies for external JS libs that are bundled into a single .js file according to dependency order */ 100 | val jsDependencies = Def.setting( 101 | Seq( 102 | "org.webjars.bower" % "react" % versions.react / "react-with-addons.js" minified "react-with-addons.min.js" commonJSName "React", 103 | "org.webjars.bower" % "react" % versions.react / "react-dom.js" minified "react-dom.min.js" dependsOn "react-with-addons.js" commonJSName "ReactDOM", 104 | "org.webjars" % "jquery" % versions.jQuery / "jquery.js" minified "jquery.min.js", 105 | "org.webjars" % "Semantic-UI" % versions.semantic / "semantic.js" minified "semantic.min.js" dependsOn "jquery.js" 106 | )) 107 | } 108 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.3.2 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += "typesafe" at "https://repo.typesafe.com/typesafe/releases/" 2 | 3 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.31") 4 | 5 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.6.1") 6 | 7 | addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.2") 8 | 9 | addSbtPlugin("se.marcuslonnberg" % "sbt-docker" % "1.4.1") 10 | 11 | addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1") 12 | 13 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.7") 14 | 15 | addSbtPlugin("com.vmunier" % "sbt-web-scalajs" % "1.0.6") 16 | 17 | addSbtPlugin("com.typesafe.sbt" % "sbt-digest" % "1.1.3") 18 | 19 | addSbtPlugin("com.typesafe.sbt" % "sbt-gzip" % "1.0.2") 20 | 21 | addSbtPlugin("com.typesafe.sbt" % "sbt-less" % "1.1.2") 22 | 23 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.3.0") 24 | 25 | addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.5.0") 26 | -------------------------------------------------------------------------------- /server/src/main/assets/images/anon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/anon.png -------------------------------------------------------------------------------- /server/src/main/assets/images/favicon-114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/favicon-114.png -------------------------------------------------------------------------------- /server/src/main/assets/images/favicon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/favicon-120.png -------------------------------------------------------------------------------- /server/src/main/assets/images/favicon-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/favicon-144.png -------------------------------------------------------------------------------- /server/src/main/assets/images/favicon-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/favicon-150.png -------------------------------------------------------------------------------- /server/src/main/assets/images/favicon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/favicon-152.png -------------------------------------------------------------------------------- /server/src/main/assets/images/favicon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/favicon-16.png -------------------------------------------------------------------------------- /server/src/main/assets/images/favicon-160.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/favicon-160.png -------------------------------------------------------------------------------- /server/src/main/assets/images/favicon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/favicon-180.png -------------------------------------------------------------------------------- /server/src/main/assets/images/favicon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/favicon-192.png -------------------------------------------------------------------------------- /server/src/main/assets/images/favicon-310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/favicon-310.png -------------------------------------------------------------------------------- /server/src/main/assets/images/favicon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/favicon-32.png -------------------------------------------------------------------------------- /server/src/main/assets/images/favicon-57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/favicon-57.png -------------------------------------------------------------------------------- /server/src/main/assets/images/favicon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/favicon-60.png -------------------------------------------------------------------------------- /server/src/main/assets/images/favicon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/favicon-64.png -------------------------------------------------------------------------------- /server/src/main/assets/images/favicon-70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/favicon-70.png -------------------------------------------------------------------------------- /server/src/main/assets/images/favicon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/favicon-72.png -------------------------------------------------------------------------------- /server/src/main/assets/images/favicon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/favicon-76.png -------------------------------------------------------------------------------- /server/src/main/assets/images/favicon-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/favicon-96.png -------------------------------------------------------------------------------- /server/src/main/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/favicon.ico -------------------------------------------------------------------------------- /server/src/main/assets/images/providers/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/providers/facebook.png -------------------------------------------------------------------------------- /server/src/main/assets/images/providers/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/providers/github.png -------------------------------------------------------------------------------- /server/src/main/assets/images/providers/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/providers/google.png -------------------------------------------------------------------------------- /server/src/main/assets/images/providers/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/providers/twitter.png -------------------------------------------------------------------------------- /server/src/main/assets/images/providers/vk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/providers/vk.png -------------------------------------------------------------------------------- /server/src/main/assets/images/providers/xing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/providers/xing.png -------------------------------------------------------------------------------- /server/src/main/assets/images/providers/yahoo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/providers/yahoo.png -------------------------------------------------------------------------------- /server/src/main/assets/images/scalafiddle-logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/scalafiddle-logo-small.png -------------------------------------------------------------------------------- /server/src/main/assets/images/scalafiddle-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/scalafiddle-logo.png -------------------------------------------------------------------------------- /server/src/main/assets/images/wait-ring.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafiddle/scalafiddle-editor/22c68296f5eaa5853eb4077e8323a310b64351a1/server/src/main/assets/images/wait-ring.gif -------------------------------------------------------------------------------- /server/src/main/assets/javascript/mousetrap.js: -------------------------------------------------------------------------------- 1 | /* mousetrap v1.6.0 craig.is/killing/mice */ 2 | (function(r,t,g){function u(a,b,h){a.addEventListener?a.addEventListener(b,h,!1):a.attachEvent("on"+b,h)}function y(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return k[a.which]?k[a.which]:p[a.which]?p[a.which]:String.fromCharCode(a.which).toLowerCase()}function D(a){var b=[];a.shiftKey&&b.push("shift");a.altKey&&b.push("alt");a.ctrlKey&&b.push("ctrl");a.metaKey&&b.push("meta");return b}function v(a){return"shift"==a||"ctrl"==a||"alt"==a|| 3 | "meta"==a}function z(a,b){var h,c,e,g=[];h=a;"+"===h?h=["+"]:(h=h.replace(/\+{2}/g,"+plus"),h=h.split("+"));for(e=0;el||k.hasOwnProperty(l)&&(n[k[l]]=l)}e=n[h]?"keydown":"keypress"}"keypress"==e&&g.length&&(e="keydown");return{key:c,modifiers:g,action:e}}function C(a,b){return null===a||a===t?!1:a===b?!0:C(a.parentNode,b)}function c(a){function b(a){a= 4 | a||{};var b=!1,m;for(m in n)a[m]?b=!0:n[m]=0;b||(w=!1)}function h(a,b,m,f,c,h){var g,e,k=[],l=m.type;if(!d._callbacks[a])return[];"keyup"==l&&v(a)&&(b=[a]);for(g=0;g":".","?":"/","|":"\\"},A={option:"alt",command:"meta","return":"enter", 9 | escape:"esc",plus:"+",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},n;for(g=1;20>g;++g)k[111+g]="f"+g;for(g=0;9>=g;++g)k[g+96]=g;c.prototype.bind=function(a,b,c){a=a instanceof Array?a:[a];this._bindMultiple.call(this,a,b,c);return this};c.prototype.unbind=function(a,b){return this.bind.call(this,a,function(){},b)};c.prototype.trigger=function(a,b){if(this._directMap[a+":"+b])this._directMap[a+":"+b]({},a);return this};c.prototype.reset=function(){this._callbacks={};this._directMap= 10 | {};return this};c.prototype.stopCallback=function(a,b){return-1<(" "+b.className+" ").indexOf(" mousetrap ")||C(b,this.target)?!1:"INPUT"==b.tagName||"SELECT"==b.tagName||"TEXTAREA"==b.tagName||b.isContentEditable};c.prototype.handleKey=function(){return this._handleKey.apply(this,arguments)};c.addKeycodes=function(a){for(var b in a)a.hasOwnProperty(b)&&(k[b]=a[b]);n=null};c.init=function(){var a=c(t),b;for(b in a)"_"!==b.charAt(0)&&(c[b]=function(b){return function(){return a[b].apply(a,arguments)}}(b))}; 11 | c.init();r.Mousetrap=c;"undefined"!==typeof module&&module.exports&&(module.exports=c);"function"===typeof define&&define.amd&&define(function(){return c})}})("undefined"!==typeof window?window:null,"undefined"!==typeof window?document:null); 12 | 13 | (function(a){var c={},d=a.prototype.stopCallback;a.prototype.stopCallback=function(e,b,a,f){return this.paused?!0:c[a]||c[f]?!1:d.call(this,e,b,a)};a.prototype.bindGlobal=function(a,b,d){this.bind(a,b,d);if(a instanceof Array)for(b=0;b * { 22 | flex: 0 0 auto; 23 | } 24 | } 25 | 26 | .stretchy { 27 | flex: 1 1 auto; 28 | } 29 | 30 | .column { 31 | flex-direction: column; 32 | } 33 | 34 | .full-screen { 35 | .flex-container; 36 | .column; 37 | 38 | > header { 39 | .flex-container; 40 | overflow: visible; 41 | height: 55px; 42 | min-width: 1000px; 43 | padding: 5px; 44 | align-items: center; 45 | box-shadow: 0 0 5px rgba(66, 66, 86, .2); 46 | z-index: 999; 47 | 48 | > .left { 49 | display: flex; 50 | flex-wrap: nowrap; 51 | flex: 1; 52 | align-items: center; 53 | 54 | > .logo { 55 | margin: 0 10px; 56 | > * > img { 57 | height: 40px; 58 | } 59 | } 60 | } 61 | 62 | > .right { 63 | display: flex; 64 | flex-wrap: nowrap; 65 | align-items: center; 66 | 67 | > .userinfo { 68 | .flex-container; 69 | overflow: visible; 70 | align-items: center; 71 | 72 | > div > .username { 73 | margin-right: 10px; 74 | cursor: pointer; 75 | 76 | &:hover { 77 | text-decoration: underline; 78 | } 79 | } 80 | } 81 | > * > .login { 82 | vertical-align: middle; 83 | padding: 8px 10px; 84 | 85 | > img { 86 | height: 24px; 87 | padding-right: 10px; 88 | vertical-align: middle; 89 | } 90 | } 91 | } 92 | 93 | } 94 | 95 | > .main { 96 | .stretchy; 97 | .flex-container; 98 | 99 | > .sidebar { 100 | .flex-container; 101 | .column; 102 | width: 250px; 103 | border-right: solid 1px #eaeaef; 104 | 105 | > div { 106 | padding: 0 10px; 107 | } 108 | 109 | > .bottom { 110 | padding: 0 10px; 111 | margin-top: auto; 112 | 113 | > .toggle { 114 | text-align: center; 115 | margin-bottom: 10px; 116 | width: 100%; 117 | } 118 | } 119 | 120 | &.folded { 121 | width: 60px; 122 | 123 | > .accordion { 124 | display: none; 125 | } 126 | } 127 | } 128 | 129 | > .editor-area { 130 | .flex-container; 131 | .stretchy; 132 | flex-direction: row; 133 | 134 | @media @mobile { 135 | flex-direction: column; 136 | } 137 | 138 | > .editor { 139 | flex: 1; 140 | position: relative; 141 | border-right: solid 1px #eaeaef; 142 | 143 | @media @mobile { 144 | border-bottom: solid 1px #eaeaef; 145 | } 146 | 147 | > #editor { 148 | width: 100%; 149 | height: 100%; 150 | position: absolute; 151 | 152 | > .ace_gutter { 153 | @media @mobile { 154 | display: none; 155 | } 156 | } 157 | } 158 | } 159 | 160 | > .output { 161 | flex: 1; 162 | position: relative; 163 | width: 100%; 164 | height: auto; 165 | > iframe { 166 | position: absolute; 167 | top: 0; 168 | bottom: 0; 169 | left: 0; 170 | right: 0; 171 | } 172 | } 173 | 174 | > div > .label { 175 | margin: 0; 176 | position: absolute; 177 | top: 5px; 178 | right: 5px; 179 | padding: 0.5em; 180 | background-color: rgba(230, 230, 230, 0.8); 181 | border-radius: .3rem; 182 | color: #aaa; 183 | z-index: 15; 184 | text-align: right; 185 | font-size: 0.8em; 186 | } 187 | 188 | > div > .optionsmenu { 189 | margin: 0; 190 | position: absolute; 191 | top: 5px; 192 | right: 5px; 193 | z-index: 15; 194 | 195 | > .optionsbutton { 196 | background-color: rgba(230, 230, 230, 0.8); 197 | } 198 | } 199 | 200 | * > .header { 201 | font-size: 1em !important; 202 | } 203 | } 204 | } 205 | } 206 | 207 | .monospace { 208 | font-family: Monaco, Menlo, 'Ubuntu Mono', Consolas, source-code-pro, monospace; 209 | text-rendering: optimizeLegibility; 210 | white-space: pre; 211 | } 212 | 213 | .button.login { 214 | background-color: #f400a1; 215 | color: #ffffff; 216 | } 217 | 218 | img.author { 219 | height: 42px; 220 | border-radius: 50%; 221 | vertical-align: middle; 222 | margin-right: 10px; 223 | } 224 | 225 | /* Override Semantic defaults */ 226 | .ui.button:not(.icon) > .icon:not(.button) { 227 | margin: 0 0.25em 0 0; 228 | } 229 | 230 | .ui.accordion .title:not(.ui) { 231 | font-weight: 600; 232 | font-size: 1.5em; 233 | } 234 | 235 | .ui.header:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6) { 236 | font-size: 1em; 237 | color: #aaa; 238 | } 239 | 240 | .ui.form .fields { 241 | margin-left: 0; 242 | } 243 | 244 | .ui.list[class*="middle aligned"] .content { 245 | position: relative; 246 | top: 0.6em; 247 | left: 0.6em; 248 | } 249 | 250 | .ui.list > .item [class*="floated"] { 251 | margin: 0; 252 | } 253 | 254 | .ui.list > .item:first-child { 255 | padding-top: inherit; 256 | } 257 | 258 | .ui.list.divided { 259 | margin-top: 0; 260 | } 261 | 262 | /* Override Ace configs */ 263 | .ace-line { 264 | white-space: nowrap; 265 | } 266 | 267 | .ace_editor { 268 | font-size: 14px; 269 | font-family: Monaco, Menlo, 'Ubuntu Mono', Consolas, source-code-pro, monospace; 270 | text-rendering: optimizeLegibility; 271 | } 272 | 273 | /* CSS for result frame */ 274 | #output { 275 | position: absolute; 276 | width: 100%; 277 | max-height: 100%; 278 | overflow: auto; 279 | color: #333; 280 | padding-left: 0.5em; 281 | padding-top: 0.2em; 282 | box-sizing: border-box; 283 | font-size: 14px; 284 | } 285 | 286 | .liblist { 287 | max-height: 350px; 288 | overflow-y: auto; 289 | border: 1px solid #ddd; 290 | padding: 2px; 291 | margin: 4px 0; 292 | 293 | * > .lib-group { 294 | margin: 0; 295 | padding: 0.5em 0.6em; 296 | background-color: #eee; 297 | color: #888; 298 | } 299 | 300 | * > .ui.basic.button, .ui.basic.button:hover { 301 | box-shadow: none !important; 302 | vertical-align: middle !important; 303 | padding-top: .9rem !important; 304 | padding-bottom: 0.3rem !important; 305 | padding-right: 0; 306 | padding-left: 4px; 307 | } 308 | * > .icon { 309 | font-size: 1.8em; 310 | } 311 | 312 | * > .ui.icon.button > .codeicon { 313 | font-size: 1.4em !important; 314 | margin-bottom: 5px !important; 315 | } 316 | } 317 | 318 | .fiddle-list { 319 | font-family: 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif; 320 | * > table { 321 | width: 100%; 322 | } 323 | /* Hiding ReactTable's pagination settings */ 324 | ._a4 { 325 | display: none !important; 326 | } 327 | } 328 | 329 | #fiddle-container { 330 | position: absolute; 331 | top: 0; 332 | bottom: 0; 333 | left: 0; 334 | right: 0; 335 | } 336 | 337 | .embed-options .menu { 338 | left: -300px !important; 339 | } 340 | 341 | .embed-editor { 342 | width: 1100px; 343 | padding: 10px; 344 | display: flex; 345 | overflow: hidden; 346 | flex-wrap: nowrap; 347 | 348 | > .options { 349 | flex: 0 0 280px; 350 | } 351 | 352 | > .preview { 353 | flex: 1; 354 | margin-left: 10px; 355 | border-left: #ddd solid 1px; 356 | padding-left: 5px; 357 | } 358 | 359 | * > .header { 360 | color: #aaaaaa; 361 | text-transform: uppercase; 362 | font-size: 0.8em; 363 | margin-bottom: 1.5em; 364 | } 365 | } 366 | 367 | .ui.form textarea.embed-code { 368 | height: 70px; 369 | color: #888; 370 | padding: 2px; 371 | } 372 | 373 | .text.white { 374 | color: #FFFFFF; 375 | } 376 | 377 | .text.grey { 378 | color: #CCCCCC; 379 | } 380 | 381 | .text.black { 382 | color: #1B1C1D; 383 | } 384 | 385 | .text.yellow { 386 | color: #F2C61F; 387 | } 388 | 389 | .text.teal { 390 | color: #00B5AD; 391 | } 392 | 393 | .text.red { 394 | color: #D95C5C; 395 | } 396 | 397 | .text.purple { 398 | color: #564F8A; 399 | } 400 | 401 | .text.pink { 402 | color: #D9499A; 403 | } 404 | 405 | .text.orange { 406 | color: #E07B53; 407 | } 408 | 409 | .text.green { 410 | color: #5BBD72; 411 | } 412 | 413 | .text.blue { 414 | color: #3B83C0; 415 | } 416 | 417 | pre.error { 418 | color: #cf040e; 419 | } 420 | 421 | ::-webkit-scrollbar { 422 | width: 10px; 423 | height: 10px; 424 | } 425 | 426 | ::-webkit-scrollbar-thumb { 427 | background: #ccc; 428 | border-radius: 20px; 429 | } 430 | 431 | ::-webkit-scrollbar-track { 432 | background: #eee; 433 | } 434 | -------------------------------------------------------------------------------- /server/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | # Config file in HOCON format. See following for more information: 2 | # https://www.playframework.com/documentation/latest/Configuration 3 | 4 | play.assets.defaultCache = "max-age=604800" 5 | play.http.secret.key = ${?APPLICATION_SECRET} 6 | 7 | play.http.filters = "scalafiddle.server.Filters" 8 | play.modules.enabled += "scalafiddle.server.modules.SilhouetteModule" 9 | 10 | play.filters.cors { 11 | pathPrefixes = ["/raw", "/oembed", "/oembed.json"] 12 | allowedHttpMethods = ["GET", "POST", "OPTIONS"] 13 | preflightMaxAge = 3 days 14 | } 15 | 16 | play.http.forwarded.trustedProxies=["0.0.0.0/0", "::/0"] 17 | 18 | play.server.netty { 19 | maxInitialLineLength = 64000 20 | } 21 | 22 | h2 { 23 | profile = "slick.jdbc.H2Profile$" 24 | db { 25 | keepAliveConnection = true 26 | connectionPool = disabled 27 | driver = "org.h2.Driver" 28 | url = "jdbc:h2:mem:tsql1;MODE=PostgreSQL;DB_CLOSE_DELAY=-1" 29 | } 30 | } 31 | 32 | postgre { 33 | profile = "slick.jdbc.PostgresProfile$" 34 | db { 35 | keepAliveConnection = true 36 | connectionPool = HikariCP 37 | driver = "org.postgresql.Driver" 38 | url = ${?SCALAFIDDLE_SQL_URL} 39 | user = ${?SCALAFIDDLE_SQL_USER} 40 | password = ${?SCALAFIDDLE_SQL_PASSWORD} 41 | } 42 | } 43 | 44 | scalafiddle { 45 | scalafiddleURL = "http://localhost:9000" 46 | scalafiddleURL = ${?SCALAFIDDLE_URL} 47 | 48 | compilerURL = "http://localhost:8880" 49 | compilerURL = ${?SCALAFIDDLE_COMPILER_URL} 50 | 51 | librariesURL = "https://raw.githubusercontent.com/scalafiddle/scalafiddle-io/master/libraries.json" 52 | librariesURL = ${?SCALAFIDDLE_LIBRARIES_URL} 53 | 54 | helpURL = "https://scalafiddle.github.io/scalafiddle-editor/Welcome.html" 55 | helpURL = ${?SCALAFIDDLE_HELP_URL} 56 | 57 | scalaVersions = ["2.11", "2.12"] 58 | defaultScalaVersion = "2.12" 59 | 60 | scalaJSVersions = ["0.6", "1"] 61 | defaultScalaJSVersion = "1" 62 | 63 | refreshLibraries = 300 64 | refreshLibraries = ${?SCALAFIDDLE_REFRESH_LIBRARIES} 65 | 66 | analyticsID = "UA-74405486-2" 67 | analyticsID = ${?SCALAFIDDLE_ANALYTICS_ID} 68 | 69 | defaultSource = 70 | """import fiddle.Fiddle, Fiddle.println 71 | import scalajs.js 72 | 73 | @js.annotation.JSExportTopLevel("ScalaFiddle") 74 | object ScalaFiddle { 75 | // $FiddleStart 76 | // Start writing your ScalaFiddle code here 77 | 78 | // $FiddleEnd 79 | } 80 | """ 81 | 82 | dbConfig = "h2" 83 | dbConfig = ${?SCALAFIDDLE_SQL_CONFIG} 84 | 85 | loginProviders = ["github"] 86 | } 87 | 88 | kamon { 89 | modules { 90 | kamon-statsd.auto-start = true 91 | kamon-statsd.auto-start = ${?SCALAFIDDLE_METRICS_STATSD} 92 | } 93 | statsd { 94 | hostname = "localhost" 95 | hostname = ${?SCALAFIDDLE_METRICS_STATSD_HOSTNAME} 96 | port = 8125 97 | port = ${?SCALAFIDDLE_METRICS_STATSD_PORT} 98 | simple-metric-key-generator.application = "scalafiddle-editor" 99 | } 100 | } 101 | 102 | include "silhouette.conf" 103 | include "local.conf" 104 | -------------------------------------------------------------------------------- /server/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %d{HH:mm:ss.SSS} %-5p [%c:%L] %m%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /server/src/main/resources/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | # Home page 6 | GET / controllers.Application.index(id = "", version = "0") 7 | GET /authenticate/:provider controllers.SocialAuthController.authenticate(provider) 8 | GET /signout controllers.Application.signOut 9 | GET /sf/:id controllers.Application.index(id, version = "0") 10 | GET /sf/:id/$version<\d+> controllers.Application.index(id, version) 11 | GET /raw/:id/$version<\d+> controllers.Application.rawFiddle(id, version) 12 | GET /html/:id/$version<\d+> controllers.Application.htmlFiddle(id, version) 13 | GET /htmlscript/:id/$version<\d+> controllers.Application.htmlScript(id, version) 14 | GET /resultframe controllers.Application.resultFrame 15 | GET /libraries/:scalaVersion controllers.Application.libraryListing(scalaVersion) 16 | GET /oembed.json controllers.Application.oembed(url, maxwidth: Option[Int], maxheight: Option[Int], format: Option[String]) 17 | GET /oembedstatic.json controllers.Application.oembedStatic(url, maxwidth: Option[Int], maxheight: Option[Int], format: Option[String]) 18 | 19 | # Map static resources from the /public folder to the /assets URL path 20 | GET /assets/stylesheets/themes/default/assets/fonts/*file controllers.Assets.at(path="/public/lib/Semantic-UI/themes/default/assets/fonts", file) 21 | GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) 22 | 23 | # Autowire calls 24 | POST /api/*path controllers.Application.autowireApi(path: String) 25 | 26 | -------------------------------------------------------------------------------- /server/src/main/resources/silhouette.conf: -------------------------------------------------------------------------------- 1 | callbackBaseURL = "http://localhost.scalafiddle.io:9000/authenticate" 2 | callbackBaseURL = ${?SCALAFIDDLE_AUTH_URL} 3 | 4 | silhouette { 5 | 6 | # Authenticator settings 7 | authenticator.cookieName="authenticator" 8 | authenticator.cookiePath="/" 9 | authenticator.secureCookie=false // Disabled for testing on localhost without SSL, otherwise cookie couldn't be set 10 | authenticator.httpOnlyCookie=true 11 | authenticator.useFingerprinting=true 12 | authenticator.authenticatorIdleTimeout=30 days 13 | authenticator.authenticatorExpiry=90 days 14 | 15 | authenticator.rememberMe.cookieMaxAge=30 days 16 | authenticator.rememberMe.authenticatorIdleTimeout=5 days 17 | authenticator.rememberMe.authenticatorExpiry=30 days 18 | 19 | authenticator.signer.key = "[changeme]" // A unique encryption key 20 | authenticator.crypter.key = "[changeme]" // A unique encryption key 21 | 22 | # OAuth1 token secret provider settings 23 | oauth1TokenSecretProvider.cookieName="OAuth1TokenSecret" 24 | oauth1TokenSecretProvider.cookiePath="/" 25 | oauth1TokenSecretProvider.secureCookie=false // Disabled for testing on localhost without SSL, otherwise cookie couldn't be set 26 | oauth1TokenSecretProvider.httpOnlyCookie=true 27 | oauth1TokenSecretProvider.expirationTime=120 minutes 28 | 29 | oauth1TokenSecretProvider.cookie.signer.key = "[changeme]" // A unique encryption key 30 | oauth1TokenSecretProvider.crypter.key = "[changeme]" // A unique encryption key 31 | 32 | # Social state handler 33 | socialStateHandler.signer.key = "[changeme]" // A unique encryption key 34 | 35 | # CSRF state item handler settings 36 | csrfStateItemHandler.cookieName="OAuth2State" 37 | csrfStateItemHandler.cookiePath="/" 38 | csrfStateItemHandler.secureCookie=false // Disabled for testing on localhost without SSL, otherwise cookie couldn't be set 39 | csrfStateItemHandler.httpOnlyCookie=true 40 | csrfStateItemHandler.expirationTime=5 minutes 41 | 42 | csrfStateItemHandler.signer.key = "[changeme]" // A unique encryption key 43 | 44 | # Facebook provider 45 | facebook.authorizationURL="https://graph.facebook.com/v2.3/oauth/authorize" 46 | facebook.accessTokenURL="https://graph.facebook.com/v2.3/oauth/access_token" 47 | facebook.redirectURL=${callbackBaseURL}/facebook 48 | facebook.clientID="" 49 | facebook.clientID=${?FACEBOOK_CLIENT_ID} 50 | facebook.clientSecret="" 51 | facebook.clientSecret=${?FACEBOOK_CLIENT_SECRET} 52 | facebook.scope="email" 53 | 54 | # Google provider 55 | google.authorizationURL="https://accounts.google.com/o/oauth2/auth" 56 | google.accessTokenURL="https://accounts.google.com/o/oauth2/token" 57 | google.redirectURL=${callbackBaseURL}/google 58 | google.clientID="" 59 | google.clientID=${?GOOGLE_CLIENT_ID} 60 | google.clientSecret="" 61 | google.clientSecret=${?GOOGLE_CLIENT_SECRET} 62 | google.scope="profile email" 63 | 64 | github { 65 | authorizationURL="https://github.com/login/oauth/authorize" 66 | accessTokenURL="https://github.com/login/oauth/access_token" 67 | redirectURL=${callbackBaseURL}/github 68 | clientID="" 69 | clientID=${?GITHUB_CLIENT_ID} 70 | clientSecret="" 71 | clientSecret=${?GITHUB_CLIENT_SECRET} 72 | } 73 | 74 | gitlab { 75 | authorizationURL="https://gitlab.com/oauth/authorize" 76 | accessTokenURL="https://gitlab.com/oauth/token" 77 | redirectURL=${callbackBaseURL}/gitlab 78 | clientID="" 79 | clientID=${?GITLAB_CLIENT_ID} 80 | clientSecret="" 81 | clientSecret=${?GITLAB_CLIENT_SECRET} 82 | scope="api" 83 | } 84 | 85 | # VK provider 86 | vk.authorizationURL="http://oauth.vk.com/authorize" 87 | vk.accessTokenURL="https://oauth.vk.com/access_token" 88 | vk.redirectURL=${callbackBaseURL}/vk 89 | vk.clientID="" 90 | vk.clientID=${?VK_CLIENT_ID} 91 | vk.clientSecret="" 92 | vk.clientSecret=${?VK_CLIENT_SECRET} 93 | vk.scope="email" 94 | 95 | # Twitter provider 96 | twitter.requestTokenURL="https://twitter.com/oauth/request_token" 97 | twitter.accessTokenURL="https://twitter.com/oauth/access_token" 98 | twitter.authorizationURL="https://twitter.com/oauth/authenticate" 99 | twitter.callbackURL=${callbackBaseURL}/twitter 100 | twitter.consumerKey="" 101 | twitter.consumerKey=${?TWITTER_CONSUMER_KEY} 102 | twitter.consumerSecret="" 103 | twitter.consumerSecret=${?TWITTER_CONSUMER_SECRET} 104 | 105 | # Xing provider 106 | xing.requestTokenURL="https://api.xing.com/v1/request_token" 107 | xing.accessTokenURL="https://api.xing.com/v1/access_token" 108 | xing.authorizationURL="https://api.xing.com/v1/authorize" 109 | xing.callbackURL=${callbackBaseURL}/xing 110 | xing.consumerKey="" 111 | xing.consumerKey=${?XING_CONSUMER_KEY} 112 | xing.consumerSecret="" 113 | xing.consumerSecret=${?XING_CONSUMER_SECRET} 114 | 115 | # Yahoo provider 116 | yahoo.providerURL="https://me.yahoo.com/" 117 | yahoo.callbackURL=${callbackBaseURL}/yahoo 118 | yahoo.axRequired={ 119 | "fullname": "http://axschema.org/namePerson", 120 | "email": "http://axschema.org/contact/email", 121 | "image": "http://axschema.org/media/image/default" 122 | } 123 | yahoo.realm="http://localhost:9000" 124 | } 125 | -------------------------------------------------------------------------------- /server/src/main/resources/tables.sql: -------------------------------------------------------------------------------- 1 | CREATE ROLE scalafiddle; 2 | 3 | CREATE DATABASE scalafiddle; 4 | 5 | -- log in to scalafiddle DB 6 | 7 | CREATE TABLE "fiddle" ( 8 | "id" VARCHAR NOT NULL, 9 | "version" INTEGER NOT NULL, 10 | "name" VARCHAR NOT NULL, 11 | "description" VARCHAR NOT NULL, 12 | "sourcecode" VARCHAR NOT NULL, 13 | "libraries" VARCHAR NOT NULL, 14 | "scala_version" VARCHAR NOT NULL, 15 | "user" VARCHAR NOT NULL, 16 | "parent" VARCHAR, 17 | "created" BIGINT NOT NULL, 18 | "removed" BOOLEAN NOT NULL 19 | ); 20 | 21 | CREATE TABLE "user" ( 22 | "user_id" VARCHAR NOT NULL, 23 | "login_info" VARCHAR NOT NULL, 24 | "first_name" VARCHAR, 25 | "last_name" VARCHAR, 26 | "full_name" VARCHAR, 27 | "email" VARCHAR, 28 | "avatar_url" VARCHAR, 29 | "activated" BOOLEAN NOT NULL 30 | ); 31 | 32 | CREATE TABLE "access" ( 33 | "id" SERIAL PRIMARY KEY, 34 | "fiddle_id" VARCHAR NOT NULL, 35 | "version" INTEGER NOT NULL, 36 | "timestamp" BIGINT NOT NULL, 37 | "user_id" VARCHAR, 38 | "embedded" BOOLEAN NOT NULL, 39 | "source_ip" VARCHAR NOT NULL 40 | ); 41 | 42 | ALTER TABLE "fiddle" 43 | ADD CONSTRAINT "pk_fiddle" PRIMARY KEY ("id", "version"); 44 | 45 | ALTER TABLE "user" 46 | ADD CONSTRAINT "pk_user" PRIMARY KEY ("user_id"); 47 | 48 | CREATE INDEX "access_id_idx" 49 | ON "access" USING BTREE (fiddle_id); 50 | 51 | CREATE INDEX "access_time_idx" 52 | ON "access" USING BTREE (timestamp); 53 | 54 | GRANT ALL ON TABLE "fiddle" TO scalafiddle; 55 | 56 | GRANT ALL ON TABLE "user" TO scalafiddle; 57 | 58 | GRANT ALL ON TABLE "access" TO scalafiddle; 59 | 60 | GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO scalafiddle; 61 | -------------------------------------------------------------------------------- /server/src/main/scala/ErrorHandler.scala: -------------------------------------------------------------------------------- 1 | import javax.inject._ 2 | 3 | import play.api._ 4 | import play.api.http.DefaultHttpErrorHandler 5 | import play.api.mvc.Results._ 6 | import play.api.mvc._ 7 | import play.api.routing.Router 8 | 9 | import scala.concurrent._ 10 | 11 | @Singleton 12 | class ErrorHandler @Inject() ( 13 | env: Environment, 14 | config: Configuration, 15 | sourceMapper: OptionalSourceMapper, 16 | router: Provider[Router] 17 | ) extends DefaultHttpErrorHandler(env, config, sourceMapper, router) { 18 | 19 | override def onProdServerError(request: RequestHeader, exception: UsefulException) = 20 | Future.successful(InternalServerError("A server error occurred: " + exception.getMessage)) 21 | 22 | override def onForbidden(request: RequestHeader, message: String) = 23 | Future.successful(Forbidden("You're not allowed to access this resource.")) 24 | 25 | override protected def onNotFound(request: RequestHeader, message: String) = 26 | Future.successful(NotFound("Resource not found")) 27 | 28 | override protected def onBadRequest(request: RequestHeader, message: String) = 29 | Future.successful(BadRequest("Bad request")) 30 | 31 | } 32 | -------------------------------------------------------------------------------- /server/src/main/scala/controllers/SocialAuthController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject.Inject 4 | 5 | import com.mohiva.play.silhouette.api._ 6 | import com.mohiva.play.silhouette.api.exceptions.ProviderException 7 | import com.mohiva.play.silhouette.impl.providers._ 8 | import kamon.Kamon 9 | import play.api.i18n.I18nSupport 10 | import play.api.mvc.InjectedController 11 | 12 | import scala.concurrent.{ExecutionContext, Future} 13 | import scalafiddle.server.models.services.UserService 14 | import scalafiddle.server.utils.auth.DefaultEnv 15 | 16 | /** 17 | * The social auth controller. 18 | * 19 | * @param silhouette The Silhouette stack. 20 | * @param userService The user service implementation. 21 | * @param socialProviderRegistry The social provider registry. 22 | */ 23 | class SocialAuthController @Inject() ( 24 | implicit val silhouette: Silhouette[DefaultEnv], 25 | ec: ExecutionContext, 26 | userService: UserService, 27 | socialProviderRegistry: SocialProviderRegistry 28 | ) extends InjectedController 29 | with I18nSupport 30 | with Logger { 31 | 32 | val loginCount = Kamon.metrics.counter("login") 33 | 34 | /** 35 | * Authenticates a user against a social provider. 36 | * 37 | * @param provider The ID of the provider to authenticate against. 38 | * @return The result to display. 39 | */ 40 | def authenticate(provider: String) = Action.async { implicit request => 41 | (socialProviderRegistry.get[SocialProvider](provider) match { 42 | case Some(p: SocialProvider with CommonSocialProfileBuilder) => 43 | p.authenticate().flatMap { 44 | case Left(result) => Future.successful(result) 45 | case Right(authInfo) => 46 | for { 47 | profile <- p.retrieveProfile(authInfo) 48 | user <- userService.save(profile) 49 | authenticator <- silhouette.env.authenticatorService.create(profile.loginInfo) 50 | value <- silhouette.env.authenticatorService.init(authenticator) 51 | result <- silhouette.env.authenticatorService.embed(value, Redirect(routes.Application.index("", "0"))) 52 | } yield { 53 | loginCount.increment() 54 | silhouette.env.eventBus.publish(LoginEvent(user, request)) 55 | result 56 | } 57 | } 58 | case _ => Future.failed(new ProviderException(s"Cannot authenticate with unexpected social provider $provider")) 59 | }).recover { 60 | case e: ProviderException => 61 | logger.error("Unexpected provider error", e) 62 | Redirect(routes.Application.index("", "0")) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /server/src/main/scala/scalafiddle/server/ApiService.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.server 2 | 3 | import akka.actor._ 4 | import akka.pattern.ask 5 | import akka.util.Timeout 6 | import kamon.Kamon 7 | 8 | import scala.concurrent.duration._ 9 | import scala.concurrent.{ExecutionContext, Future} 10 | import scala.util.{Failure, Success, Try} 11 | import scalafiddle.server.dao.Fiddle 12 | import scalafiddle.server.models.User 13 | import scalafiddle.shared.{FiddleVersions, _} 14 | 15 | class ApiService(persistence: ActorRef, user: Option[User], _loginProviders: Seq[LoginProvider])( 16 | implicit ec: ExecutionContext 17 | ) extends Api { 18 | implicit val timeout = Timeout(15.seconds) 19 | 20 | val saveCount = Kamon.metrics.counter("fiddle-save") 21 | val updateCount = Kamon.metrics.counter("fiddle-update") 22 | val forkCount = Kamon.metrics.counter("fiddle-fork") 23 | 24 | override def save(fiddle: FiddleData): Future[Either[String, FiddleId]] = { 25 | saveCount.increment() 26 | ask(persistence, AddFiddle(fiddle, user.fold("anonymous")(_.userID))).mapTo[Try[FiddleId]].map { 27 | case Success(fid) => Right(fid) 28 | case Failure(ex) => Left(ex.toString) 29 | } 30 | } 31 | 32 | override def update(fiddle: FiddleData, id: String): Future[Either[String, FiddleId]] = { 33 | // allow update only when user is the same as fiddle author (or fiddle is anonymous) 34 | val updateOk = (fiddle.author.map(_.id), user.map(_.userID)) match { 35 | case (Some(authorId), Some(userId)) if authorId == userId => true 36 | case (None, _) => true 37 | case _ => false 38 | } 39 | if (updateOk) { 40 | updateCount.increment() 41 | ask(persistence, UpdateFiddle(fiddle, id)).mapTo[Try[FiddleId]].map { 42 | case Success(fid) => Right(fid) 43 | case Failure(ex) => Left(ex.toString) 44 | } 45 | } else { 46 | Future.successful(Left("Not allowed to update fiddle")) 47 | } 48 | } 49 | 50 | override def fork(fiddle: FiddleData, id: String, version: Int): Future[Either[String, FiddleId]] = { 51 | forkCount.increment() 52 | ask(persistence, ForkFiddle(fiddle, id, version, user.fold("anonymous")(_.userID))).mapTo[Try[FiddleId]].map { 53 | case Success(fid) => Right(fid) 54 | case Failure(ex) => Left(ex.toString) 55 | } 56 | } 57 | 58 | override def loginProviders(): Seq[LoginProvider] = _loginProviders 59 | 60 | override def userInfo(): UserInfo = 61 | user.fold(UserInfo("", "", None, loggedIn = false))(u => 62 | UserInfo(u.userID, u.name.getOrElse("Anonymous"), u.avatarURL, loggedIn = true) 63 | ) 64 | 65 | override def listFiddles(): Future[Seq[FiddleVersions]] = { 66 | user match { 67 | case Some(currentUser) => 68 | ask(persistence, FindUserFiddles(currentUser.userID)).mapTo[Try[Map[String, Seq[Fiddle]]]].map { 69 | case Success(fiddles) => 70 | fiddles.toList.map { 71 | case (id, versions) => 72 | val latest = versions.last 73 | FiddleVersions(id, latest.name, latest.libraries, latest.version, latest.created) 74 | } 75 | case Failure(e) => 76 | Seq.empty 77 | } 78 | case None => 79 | Future.successful(Seq.empty) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /server/src/main/scala/scalafiddle/server/Filters.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.server 2 | 3 | import javax.inject.Inject 4 | import play.api.http.DefaultHttpFilters 5 | import play.filters.cors.CORSFilter 6 | import play.filters.gzip.GzipFilter 7 | 8 | class Filters @Inject() (corsFilter: CORSFilter, gzipFilter: GzipFilter) extends DefaultHttpFilters(corsFilter, gzipFilter) 9 | -------------------------------------------------------------------------------- /server/src/main/scala/scalafiddle/server/HTMLFiddle.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.server 2 | 3 | import scalafiddle.shared.FiddleData 4 | 5 | object HTMLFiddle { 6 | val fiddleStart = """\s*// \$FiddleStart\s*$""".r 7 | val fiddleEnd = """\s*// \$FiddleEnd\s*$""".r 8 | 9 | // separate source code into pre,main,post blocks 10 | def extractCode(src: String): (List[String], List[String], List[String]) = { 11 | val lines = src.split("\n") 12 | val (pre, main, post) = lines.foldLeft((List.empty[String], List.empty[String], List.empty[String])) { 13 | case ((preList, mainList, postList), line) => 14 | line match { 15 | case fiddleStart() => 16 | (line :: mainList ::: preList, Nil, Nil) 17 | case fiddleEnd() if preList.nonEmpty => 18 | (preList, mainList, line :: postList) 19 | case l if postList.nonEmpty => 20 | (preList, mainList, line :: postList) 21 | case _ => 22 | (preList, line :: mainList, postList) 23 | } 24 | } 25 | (pre.reverse, main.reverse, post.reverse) 26 | } 27 | 28 | def classFor(hl: Highlighter.HighlightCode): String = { 29 | import Highlighter._ 30 | hl match { 31 | case Normal => "hl-n" 32 | case Comment => "hl-c" 33 | case Type => "hl-t" 34 | case LString => "hl-s" 35 | case Literal => "hl-l" 36 | case Keyword => "hl-k" 37 | case Reset => "hl-r" 38 | } 39 | } 40 | 41 | val logo = 42 | "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJ8AAAAoCAYAAADg1CgtAAALpUlEQVR42u1cCXQV1Rn+37x5UDhCtYpUtLSIHgiISO1yBFpOsWxSSlvKprVaQZYiIRRZNAiEhMgii0AChEAWEggIYa/s0ADdZBHasgr0sFoCGIRHyAK3/533j9zcd2fevJeHtHHuOd8h3Dv/nZl7v/nXmQfXIQvuMnREX8R3ozHf57AQGCxF7IF8GAbVQAe3/Z+2u0S4+xAdEZMQpxEM8Wx0iLcG8QHEQ3d381zyZVVHNEa8jEhGbEMUEeFENKoc8TLgFqxA4q2HdtDM3bivGPk8iG8gWiNiEVMQGxEXFERTISZS4l2DTCiDXCTeVugAT7mbVsXJ9yCiFaI/abN8xDGE3yHRokY+Trwb+C+D3bAMYt0Nq2LkewjRCzEOsQRxqJIkiyr5/Eg+Bn9EgzvS3awqSL6P7wLRokI+7ufxAKMY8tyNqqLkO/0lka+pvXnNQL9uEZJtneHbBbDTMLedoLm7UVWUfIfuleYLEI4HEssRBYgNsBqGw9vwSwMJ0BPiMMRwm0u+SFFEQUu1YC23CbEKzkAKJMHvoB08626IS77QuAHZrBgWsVLIYbdhMWOwDP9dwjA6vY3jxxFzEL9F3K8m3QYMI8Yj4ZpDDajmboRLvmD4EZxkJQg0kUiyJYilBuF4/2VYwE55UthOLbnwoDbttVLIfcIfyAkqqhM8Sfwhkc7Vci75BPJxbXaTSBbQZksReQbxruH4p5757Ig2i+3RprCtWiJb4Y1n8/U/sCm+gSzR19ef743veBlJhtqwQp7uukG8bUYw0Rl+4C662wINiXKSE4wTjZOOa7qLkM5OeGaz/dpU9ictma31jmE5+gg2V49j032DWJLvdTbe15e9h6RL04eyfO9odlSbyYk7pQhNqx+yvzCzt4yAYjtshmRoA43dBXfbnXYe0v59QJvOdmgT2DokWa4+8guSTfD1Ywm+PmySbwCb6RtsaLkl+ii2wZvA/qJNYoeRcJeQqLcMLZnHzfC4O8TjlQlOvLWQD6PchXZbcMvVR9Sfp8f1mq3HzkrRY8+l6rEsU38TtVk82+JNNEh2EMl5Ev26QvTvuHkO+H4Bk4w+nmGuyXSPq1iZ2AbvQE93kd2mboWQ/vAVWNjpKmSOLvSk773gSWNXIQO1WDZptADJuA9YhpEt9wkFsskYZ2o9hvquEH29r7nv27nNxuc7wdMlJQiu0coR/O9ie5LZki+g9XbAW9BVdcrqiFaI1xEDEM8jHvofWIqXETsRmxFbEdEKx5si+EK0DwF+TAzJxCEKEFsQmxD1wzjf0ErIdkTsoDXYQf+PxryW0e6pKCaUDfLdhg/gLMyFmgbPKrTRhpsJyM2KuIFYieiGuFeqcrp0TT+P0ryLFfdrhSySWSb1Px3G+fIrITtEkh0SpXkjy/OFS77AywA74X14RTyNRtrEyQasv0fkGyddR/sozZsVBvlyokC+dEk2JgzZPpJsnyjN+6WQb1opPujXYTm0rphWiVcs9DXSeHJ/chUjX7riHm9ZkC+NZPoh1pE1WIF4zCVfaLzK8KEtRu0nNF47uySR7jeI2og6iO70xPOxIoTH5nJrIJ5E8NdcvunwFvk5+Hv3TRAPRIl8/DoaIFqQ7+MLgww/QzxOmyeCa7d6YW4fX4NnpLXIcEiSr9M5v3OXyFeLjmlGexA++QLltUDlgwcjxUa1I1M85jxiPuL5gMndBLtgvPhVWYx00YctLpYHHj+1GONzzENcFOa5idhLWkJu3yIyfSRpVz85zC9GQD7uOvwCsYiuw9ReZYjTiBTEow7IV8cBqcYgjiIOIPYR0cXGvyVYTWtgrkUePdBjQ5CkBl1rEY2XU6DVlPagsuR7jpRJkeTTB6+7ST6ztMYj33KhhhvI5eUYJLwAaeWXYUEBEnAaygxCPEMfEBm5vVLIxZ1Yi+qggUwc8aL5Zk222ChV+7WwyFbYgLiPjudP820H/tWkMMmX6WDO84ooUN60+x3cs+zziV9M8fpkscX5+cO41oYk/Nz/tJC9ilhKZIyUfIPDCKr447v4DCdZOSWLiyCDnfXMY0e1WWyfNpUVaO+y9d6xLEsfzqse5R9p7z3M38HzK14I5bm9U0bQCHZm10QJhfTvIL5nsQnNLG7gcwsCApnWMurjmiOJiPUuLbAo0yYM8o0UxhbT8RxrJLmCEOTri/iJkGLh6YwXEA862Gj+gF1xuB4qkqxXjN+UfNBbEZKvnWJ/55CiOSuNTTMkjnpm7i3Qktk6JFiePoot1N9kqfqQoPJaih7LdmsTuZlt6DdyeSrybUGd313ltL3k4Ingqr+LJLdDQbAW5E8MpIU7SzfTW5DrRpsstyaWT2Fo8nFzlWjhay6XZBuGCDhUeMHBRsupkFJEf1qPloi/2pCvuWJsBqIumXVVVB4O+U4KY59KrgL3wfZI8o1gizcRJvr6t5yjD5kwT487w+u6GUjAZd63jRrun5Fwh7T3jVenuCkugUUx1h90/w3VS28rU8IHTjjYhDfo+AZS/yeKOetT0tqqNSKf8PeUt6sj+Y3bIww4+FyDiOD8GoZLsp0jIF9XG5kGFg/jAIVfelg6xkw7JEv9uYr7yo2QfD+UxqYq5u4mHTOeE6fFZVj44lXISECC7eflNW56/WiCAzXcvAp+3zXF6/B3PuheB23tv6v1UiUhy8IUm6hJjmukKZi2iF0Wc5cIf28Kg3xcoU803OTgOcuk/3ew2TT+EP0DcYhwBHGcqj5WMmaq5bjQV2zx4MlprSct8o1tFbKtIyTfG9LYGcS/EMcIh+i+xWNW4R1knzfLa2awUWpfww0iHw80ynGudoZVc9xqkIZYqdjMDhR9iX0jHM77I4U/tJH8S9lf2hgG+WR/aT9du8qBb2+zaar3yjwh/MTHFKbtM4v7l7XwE9SfI/V/XyHbKkLyDQ0jkW5iFyfPJ5X9ECgQaMywIwRPUbwVRnQ3iLSf2PehA+J5KOIzZf4u+WheKQWw2oZ8oibqLDnkfUNom+dsNu3bESSKzQj6gNBXrkjBgGHOKso+Qv1zpf7XFLJtpGN6OLgmsy7OpHXtTuQ10YX80i5U128ZSZI5Rv5FAf7qFPf1bLLDpsrlOath9PSbH248onCUX6Wxy1J/bymKTkUk0CZopE3F40crzP5NiZxWm9bFou5bqri/yZJs90pWBtKDnPNAmyX1bxHWESjhLNfOWwoKQOw/JwVGPA2zWzpGfBFzgTQm+lf1pLHNFoqhc2UrHDEVf1EgGx/BTejVW+WHjahUpXbPkckqU4w1tMkb5ZCWOiL1ryQCXhD6zlIUWZfIpDKRCXSuJKn/BJW7eiuuYz75UjEKR57RQ1M3iuRrIgRQ8rmO0bWnWazzCVoXUBDTTw9wMu2Hyj9+2sJsHyBCDqLxeYoafSuyPL2kfdkYKfmeEsl3Gy3mRbyGuka1RtlSw/QFxkryKxzKmdoywSKXZf59UhpbLkTjqnkvUZR81ebcVxT+ZFOLcpcTx9hOZmyIdbigCIpqC4GYnWypgoRdQ5z3FI1XV0TazKJA8FKk5GsUrPk2omroZLeY7SmM99vc+H+g4is8YkuSolS5XNdBUu+rLI4dSEQS+9YIZny3hRyQg/6ZYmwflfNGWwQWkbyKFEomXqpEmDhIdWNZC9aS/NfTCtkiuseeoH61rI4UbYtkN2vbtanMZ7XHHyN+HInZ5YFJHEIL9vm2o7rp4SQSfYCIOII0VCKZ0B7SAoFFvTYOMRORTWmPX9kc34mSqJlEjMZSSiGRrqGdJNePap/T6XxNyVc0C/nDac4Zkm/no+MTyVeqJZxrCCWD+zgsrzmReZzWkZvo2YLmN4v6w+la4gSza7aaFDTNJvlhUmD2irA+j0oZijiyZlPoYW6omD+Gri2V1iqJ9iPoxYISgWD870LEPsRqxGTEYPpVUc3qd1b476rwn7dwm9vCakieMfRjj/z3+OohaoT7a1Iu+dwWUWOMuXBxT+AugguXfC6+evgv/31fVmose5sAAAAASUVORK5CYII=" 43 | 44 | val codeStyle = 45 | """html, body, object, embed, iframe { 46 | | margin: 0; 47 | | padding: 0; 48 | | height: 100%; 49 | | width: 100%; 50 | | background: white; 51 | | color: white; 52 | | box-sizing: border-box; 53 | |} 54 | |body { 55 | | position: relative; 56 | | font-size: 16px; 57 | | color: #333; 58 | | text-align: left; 59 | | direction: ltr; 60 | |} 61 | |.sf-file { 62 | | height: 100%; 63 | | margin-bottom: 1em; 64 | | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 65 | | border: 1px solid #ddd; 66 | | border-bottom: 1px solid #ccc; 67 | | border-radius: 3px; 68 | | display: flex; 69 | | flex-direction: column; 70 | | box-sizing: border-box; 71 | |} 72 | |.sf-data { 73 | | word-wrap: normal; 74 | | background-color: #fff; 75 | | flex: 1; 76 | | overflow: auto; 77 | |} 78 | |.sf-data table { 79 | | border-collapse: collapse; 80 | | padding: 0; 81 | | margin: 0; 82 | | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 83 | | font-size: 12px; 84 | | font-weight: normal; 85 | | line-height: 1.4; 86 | | color: #333; 87 | | background: #fff; 88 | | border: 0; 89 | |} 90 | |.sf-meta { 91 | | padding: 5px; 92 | | overflow: hidden; 93 | | color: #586069; 94 | | background-color: #f7f7f7; 95 | | border-radius: 0 0 2px 2px; 96 | | box-shadow: 0 2px 4px 0px rgba(199, 199, 222, 0.5); 97 | | z-index: 1; 98 | | flex-shrink: 1; 99 | |} 100 | |.sf-meta a { 101 | | font-weight: 600; 102 | | color: #666; 103 | | text-decoration: none; 104 | | border: 0; 105 | |} 106 | |.line-number { 107 | | min-width: inherit; 108 | | padding: 1px 10px !important; 109 | | background: transparent; 110 | | width: 1%; 111 | | font-size: 12px; 112 | | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 113 | | line-height: 20px; 114 | | color: rgba(27,31,35,0.3); 115 | | text-align: right; 116 | | white-space: nowrap; 117 | | vertical-align: top; 118 | | cursor: pointer; 119 | | -webkit-user-select: none; 120 | | -moz-user-select: none; 121 | | -ms-user-select: none; 122 | | user-select: none; 123 | |} 124 | |.line-number::before { 125 | | content: attr(data-line-number); 126 | |} 127 | |.line-code { 128 | | padding: 1px 10px !important; 129 | | text-align: left; 130 | | background: transparent; 131 | | border: 0; 132 | | overflow: visible; 133 | | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 134 | | font-size: 12px; 135 | | color: #24292e; 136 | | word-wrap: normal; 137 | | white-space: pre; 138 | |} 139 | |.hl-k { 140 | | color: #e41d79; 141 | |} 142 | |.hl-n { 143 | | color: #24292e; 144 | |} 145 | |.hl-s { 146 | | color: #0eab5c; 147 | |} 148 | |.hl-c { 149 | | color: #969896; 150 | |} 151 | |.hl-t { 152 | | color: #815abb; 153 | |} 154 | |.hl-l { 155 | | color: #00b1ec; 156 | |} 157 | """.stripMargin 158 | 159 | def process(fd: FiddleData, fiddleURL: String, fiddleId: String): String = { 160 | import scalatags.Text.all._ 161 | import scalatags.Text.tags2.{style => styleTag, title => titleTag} 162 | 163 | val name = fd.name.replaceAll("\\s", "") match { 164 | case empty if empty.isEmpty => "Anonymous" 165 | case nonEmpty => nonEmpty 166 | } 167 | val codeHtml = { 168 | // highlight source code 169 | val (pre, main, post) = extractCode(fd.sourceCode) 170 | // remove indentation 171 | val indentation = 172 | main.foldLeft(99)((ind, line) => if (line.isEmpty) ind else line.takeWhile(_ == ' ').length min ind) 173 | val highlighted = Highlighter 174 | .defaultHighlightIndices((pre ::: main.map(_.drop(indentation)) ::: post).mkString("\n").toCharArray) 175 | .slice(pre.size, pre.size + main.size) 176 | // generate HTML for code lines 177 | 178 | highlighted.zipWithIndex.map { 179 | case (line, lineNo) => 180 | tr( 181 | td(cls := "line-number", data("line-number") := lineNo + 1), 182 | td(cls := "line-code")(line.map { 183 | case (content, highlight) => 184 | span(cls := classFor(highlight))(content) 185 | }) 186 | ) 187 | } 188 | } 189 | // create the final HTML 190 | html( 191 | head( 192 | titleTag(s"ScalaFiddle - $name"), 193 | styleTag(raw(codeStyle)), 194 | base(target := "_blank") 195 | ), 196 | body( 197 | div(cls := "sf-file")( 198 | div(cls := "sf-meta")( 199 | a(href := fiddleURL, style := "float:right")(img(src := logo, height := 20, alt := "ScalaFiddle")), 200 | a(href := fiddleURL)(name) 201 | ), 202 | div(cls := "sf-data")( 203 | table( 204 | tbody(codeHtml) 205 | ) 206 | ) 207 | ), 208 | script(raw(s"""setTimeout(function() { 209 | | var dataDiv = document.querySelector(".sf-data table"); 210 | | var metaDiv = document.querySelector(".sf-meta"); 211 | | var data = {height:Math.min(dataDiv.scrollHeight + metaDiv.scrollHeight + 2, 600), fiddleId:"$fiddleId"}; 212 | | window.parent.postMessage(["embedHeight", data], "*"); 213 | |}, 10); 214 | """.stripMargin)) 215 | ) 216 | ).render 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /server/src/main/scala/scalafiddle/server/Highlighter.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.server 2 | 3 | /** 4 | * Borrowed from https://github.com/lihaoyi/Ammonite/blob/master/amm/repl/src/main/scala/ammonite/repl/Highlighter.scala 5 | */ 6 | import fastparse.all._ 7 | 8 | import scala.annotation.tailrec 9 | import scalaparse.Scala._ 10 | import scalaparse.syntax.Identifiers._ 11 | 12 | object Highlighter { 13 | sealed trait HighlightCode 14 | 15 | case object Normal extends HighlightCode 16 | case object Comment extends HighlightCode 17 | case object Type extends HighlightCode 18 | case object LString extends HighlightCode 19 | case object Literal extends HighlightCode 20 | case object Keyword extends HighlightCode 21 | case object Reset extends HighlightCode 22 | 23 | object BackTicked { 24 | private[this] val regex = "`([^`]+)`".r 25 | def unapplySeq(s: Any): Option[List[String]] = { 26 | regex.unapplySeq(s.toString) 27 | } 28 | } 29 | 30 | def defaultHighlightIndices(buffer: Array[Char]) = Highlighter.defaultHighlightIndices0( 31 | CompilationUnit, 32 | buffer 33 | ) 34 | 35 | def defaultHighlightIndices0(parser: P[_], buffer: Array[Char]) = Highlighter.highlightIndices( 36 | parser, 37 | buffer, { 38 | case Operator | SymbolicKeywords => Keyword 39 | case Literals.Expr.Interp | Literals.Pat.Interp => Reset 40 | case Literals.Comment => Comment 41 | case Literals.Expr.String => LString 42 | case ExprLiteral => Literal 43 | case TypeId => Type 44 | case BackTicked(body) if alphaKeywords.contains(body) => Keyword 45 | }, 46 | Reset 47 | ) 48 | 49 | type HighlightLine = Vector[(String, HighlightCode)] 50 | def highlightIndices( 51 | parser: Parser[_], 52 | buffer: Array[Char], 53 | ruleColors: PartialFunction[Parser[_], HighlightCode], 54 | endColor: HighlightCode 55 | ): Vector[HighlightLine] = { 56 | val indices = collection.mutable.Buffer((0, endColor)) 57 | var done = false 58 | val input = buffer.mkString 59 | parser.parse( 60 | input, 61 | instrument = (rule, idx, res) => { 62 | for (color <- ruleColors.lift(rule)) { 63 | val closeColor = indices.last._2 64 | val startIndex = indices.length 65 | indices += ((idx, color)) 66 | 67 | res() match { 68 | case s: Parsed.Success[_] => 69 | val prev = indices(startIndex - 1)._1 70 | 71 | if (idx < prev && s.index <= prev) { 72 | indices.remove(startIndex, indices.length - startIndex) 73 | } 74 | while (idx < indices.last._1 && s.index <= indices.last._1) { 75 | indices.remove(indices.length - 1) 76 | } 77 | indices += ((s.index, closeColor)) 78 | if (s.index == buffer.length) done = true 79 | case f: Parsed.Failure 80 | if f.index == buffer.length 81 | && (WL ~ End).parse(input, idx).isInstanceOf[Parsed.Failure] => 82 | // EOF, stop all further parsing 83 | done = true 84 | case _ => // hard failure, or parsed nothing. Discard all progress 85 | indices.remove(startIndex, indices.length - startIndex) 86 | } 87 | } 88 | } 89 | ) 90 | // Make sure there's an index right at the start and right at the end! This 91 | // resets the colors at the snippet's end so they don't bleed into later output 92 | indices += ((999999999, endColor)) 93 | // create spans 94 | val spans = indices 95 | .sliding(2) 96 | .map { 97 | case Seq((i0, Reset), (i1, c1)) => 98 | (new String(buffer.slice(i0, i1)), Normal) 99 | case Seq((i0, c0), (i1, Reset)) => 100 | (new String(buffer.slice(i0, i1)), c0) 101 | case Seq((i0, c0), (i1, _)) => 102 | (new String(buffer.slice(i0, i1)), c0) 103 | } 104 | .filter(_._1.nonEmpty) 105 | .toVector 106 | // split lines 107 | val (sourceLines, lastLine) = 108 | spans.foldLeft((Vector.empty[HighlightLine], Vector.empty[(String, HighlightCode)])) { 109 | case ((lines, line), (str, code)) => 110 | if (str.contains('\n')) { 111 | @tailrec def splitLine( 112 | lines: Vector[HighlightLine] = lines, 113 | line: HighlightLine = line, 114 | str: String = str 115 | ): (Vector[HighlightLine], HighlightLine) = { 116 | if (str.isEmpty) { 117 | (lines, line) 118 | } else if (str.contains('\n')) { 119 | val pre = str.substring(0, str.indexOf('\n')) 120 | val rem = str.substring(str.indexOf('\n') + 1) 121 | splitLine(lines :+ (line :+ ((pre, code))), Vector.empty, rem) 122 | } else { 123 | (lines, line :+ ((str, code))) 124 | } 125 | } 126 | splitLine() 127 | } else { 128 | (lines, line :+ ((str, code))) 129 | } 130 | } 131 | 132 | sourceLines :+ lastLine 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /server/src/main/scala/scalafiddle/server/Librarian.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.server 2 | 3 | import org.slf4j.LoggerFactory 4 | import upickle.Js 5 | import upickle.default._ 6 | 7 | import scala.io.BufferedSource 8 | import scalafiddle.shared._ 9 | 10 | class Librarian(libSource: () => BufferedSource) { 11 | 12 | case class LibraryVersion( 13 | version: String, 14 | scalaVersions: Seq[String], 15 | scalaJSVersions: Seq[String], 16 | extraDeps: Seq[String], 17 | organization: Option[String], 18 | artifact: Option[String], 19 | doc: Option[String], 20 | example: Option[String] 21 | ) 22 | 23 | implicit val libraryVersionReader = upickle.default.Reader[LibraryVersion] { 24 | case Js.Obj(valueSeq @ _*) => 25 | val values = valueSeq.toMap 26 | LibraryVersion( 27 | readJs[String](values("version")), 28 | readJs[Seq[String]](values("scalaVersions")), 29 | values.get("scalaJSVersions").map(readJs[Seq[String]]).getOrElse(Seq("0.6")), 30 | readJs[Seq[String]](values.getOrElse("extraDeps", Js.Arr())), 31 | values.get("organization").map(readJs[String]), 32 | values.get("artifact").map(readJs[String]), 33 | values.get("doc").map(readJs[String]), 34 | values.get("example").map(readJs[String]) 35 | ) 36 | } 37 | 38 | case class LibraryDef( 39 | name: String, 40 | organization: String, 41 | artifact: String, 42 | doc: String, 43 | versions: Seq[LibraryVersion], 44 | compileTimeOnly: Boolean 45 | ) 46 | 47 | case class LibraryGroup( 48 | group: String, 49 | libraries: Seq[LibraryDef] 50 | ) 51 | 52 | val repoSJSRE = """([^ %]+) *%%% *([^ %]+) *% *([^ %]+)""".r 53 | val repoRE = """([^ %]+) *%% *([^ %]+) *% *([^ %]+)""".r 54 | val log = LoggerFactory.getLogger(getClass) 55 | var libraries = Seq.empty[Library] 56 | refresh() 57 | 58 | def refresh(): Unit = { 59 | try { 60 | log.debug(s"Loading libraries...") 61 | val newLibs = loadLibraries 62 | log.debug(s" loaded ${newLibs.size} libraries") 63 | libraries = newLibs 64 | } catch { 65 | case e: Throwable => 66 | log.error(s"Error loading libraries", e) 67 | } 68 | } 69 | 70 | def loadLibraries: Seq[Library] = { 71 | val data = libSource().mkString 72 | val libGroups = read[Seq[LibraryGroup]](data) 73 | for { 74 | (group, idx) <- libGroups.zipWithIndex 75 | lib <- group.libraries 76 | versionDef <- lib.versions 77 | } yield { 78 | Library( 79 | lib.name, 80 | versionDef.organization.getOrElse(lib.organization), 81 | versionDef.artifact.getOrElse(lib.artifact), 82 | versionDef.version, 83 | lib.compileTimeOnly, 84 | versionDef.scalaVersions, 85 | versionDef.scalaJSVersions, 86 | versionDef.extraDeps, 87 | f"$idx%02d:${group.group}", 88 | createDocURL(versionDef.doc.getOrElse(lib.doc)), 89 | versionDef.example 90 | ) 91 | } 92 | } 93 | 94 | def findLibrary(libDef: String): Option[Library] = libDef match { 95 | case repoSJSRE(group, artifact, version) => 96 | libraries.find(l => l.organization == group && l.artifact == artifact && l.version == version && !l.compileTimeOnly) 97 | case repoRE(group, artifact, version) => 98 | libraries.find(l => l.organization == group && l.artifact == artifact && l.version == version && l.compileTimeOnly) 99 | case _ => 100 | None 101 | } 102 | 103 | private def createDocURL(doc: String): String = { 104 | val githubRef = """([^/]+)/([^/]+)""".r 105 | doc match { 106 | case githubRef(org, lib) => s"https://github.com/$org/$lib" 107 | case _ => doc 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /server/src/main/scala/scalafiddle/server/Persistence.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.server 2 | 3 | import java.util.UUID 4 | import javax.inject.Inject 5 | 6 | import akka.actor.{Actor, ActorLogging} 7 | import akka.pattern.pipe 8 | import com.mohiva.play.silhouette.api.LoginInfo 9 | import play.api.Configuration 10 | import slick.basic.DatabaseConfig 11 | import slick.jdbc.JdbcProfile 12 | import slick.dbio.{DBIOAction, NoStream} 13 | 14 | import scala.util.{Failure, Success, Try} 15 | import scala.concurrent.ExecutionContext.Implicits.global 16 | import scala.concurrent.Future 17 | import scalafiddle.server.dao.{Fiddle, FiddleAccess, FiddleDAL} 18 | import scalafiddle.server.models.User 19 | import scalafiddle.shared.{FiddleData, FiddleId, Library} 20 | 21 | case class AddFiddle(fiddle: FiddleData, user: String) 22 | 23 | case class UpdateFiddle(fiddle: FiddleData, id: String) 24 | 25 | case class ForkFiddle(fiddle: FiddleData, id: String, version: Int, user: String) 26 | 27 | case class FindFiddle(id: String, version: Int) 28 | 29 | case class FindLastFiddle(id: String) 30 | 31 | case class FindUserFiddles(userId: String) 32 | 33 | case object GetFiddleInfo 34 | 35 | case class FiddleInfo(name: String, id: FiddleId) 36 | 37 | case class RemoveFiddle(id: String, version: Int) 38 | 39 | case class FindUser(id: String) 40 | 41 | case class FindUserLogin(loginInfo: LoginInfo) 42 | 43 | case class AddUser(user: User) 44 | 45 | case class UpdateUser(user: User) 46 | 47 | case class AddAccess(fiddleId: String, version: Int, userId: Option[String], embedded: Boolean, sourceIP: String) 48 | 49 | case class FindAccesses(fiddleId: String) 50 | 51 | case class FindAccessesBetween(startTime: Long, endTime: Long = System.currentTimeMillis()) 52 | 53 | class Persistence @Inject() (config: Configuration) extends Actor with ActorLogging { 54 | val dbConfig = DatabaseConfig.forConfig[JdbcProfile](config.get[String]("scalafiddle.dbConfig")) 55 | val db = dbConfig.db 56 | val dal = new FiddleDAL(dbConfig.profile) 57 | 58 | def runAndReply[T, R](stmt: DBIOAction[T, NoStream, Nothing])(process: T => Try[R]) = { 59 | db.run(stmt).map(r => process(r)).recover { 60 | case e: Throwable => 61 | log.error(e, s"Error accessing database ${stmt.toString}") 62 | Failure(e) 63 | } 64 | } 65 | 66 | val mapping = ('0' to '9') ++ ('A' to 'Z') ++ ('a' to 'z') 67 | 68 | def createId = { 69 | var id = math.abs(UUID.randomUUID().getLeastSignificantBits) 70 | // encode to 0-9A-Za-z 71 | var strId = "" 72 | while (strId.length < 7) { 73 | strId += mapping((id % mapping.length).toInt) 74 | id /= mapping.length 75 | } 76 | strId 77 | } 78 | 79 | def receive = { 80 | case AddFiddle(fiddle, user) => 81 | val id = createId 82 | val newFiddle = Fiddle( 83 | id, 84 | 0, 85 | fiddle.name, 86 | fiddle.description, 87 | fiddle.sourceCode, 88 | fiddle.libraries.map(Library.stringify).toList, 89 | fiddle.scalaVersion, 90 | fiddle.scalaJSVersion, 91 | user 92 | ) 93 | runAndReply(dal.insertFiddle(newFiddle))(r => Success(FiddleId(id, 0))) pipeTo sender() 94 | 95 | case ForkFiddle(fiddle, id, version, user) => 96 | val id = createId 97 | val newFiddle = Fiddle( 98 | id, 99 | 0, 100 | fiddle.name, 101 | fiddle.description, 102 | fiddle.sourceCode, 103 | fiddle.libraries.map(Library.stringify).toList, 104 | fiddle.scalaVersion, 105 | fiddle.scalaJSVersion, 106 | user, 107 | Some(s"$id/$version") 108 | ) 109 | runAndReply(dal.insertFiddle(newFiddle))(r => Success(FiddleId(id, 0))) pipeTo sender() 110 | 111 | case FindFiddle(id, version) => 112 | val res = db 113 | .run(dal.findFiddle(id, version)) 114 | .map { 115 | case Some(fiddle) => Success(fiddle) 116 | case None => Failure(new NoSuchElementException) 117 | } 118 | .recover { 119 | case e: Throwable => Failure(e) 120 | } 121 | res pipeTo sender() 122 | 123 | case FindUserFiddles(userId) => 124 | val res = db.run(dal.findUserFiddles(userId)).map { fiddles => 125 | Success(fiddles.groupBy(_.id).map { case (id, versions) => id -> versions.sortBy(_.version) }) 126 | } recover { 127 | case e: Throwable => Failure(e) 128 | } 129 | res pipeTo sender() 130 | 131 | case UpdateFiddle(fiddle, id) => 132 | // find last fiddle version for this id 133 | val res = db 134 | .run(dal.findFiddleVersions(id)) 135 | .flatMap { 136 | case fiddles if fiddles.nonEmpty => 137 | val latest = fiddles.last 138 | val newVersion = latest.version + 1 139 | val newFiddle = Fiddle( 140 | id, 141 | newVersion, 142 | fiddle.name, 143 | fiddle.description, 144 | fiddle.sourceCode, 145 | fiddle.libraries.map(Library.stringify).toList, 146 | fiddle.scalaVersion, 147 | fiddle.scalaJSVersion, 148 | latest.user, 149 | Some(s"${latest.id}/${latest.version}") 150 | ) 151 | runAndReply(dal.insertFiddle(newFiddle))(r => Success(FiddleId(id, newVersion))) 152 | case _ => Future.successful(Failure(new NoSuchElementException)) 153 | } 154 | .recover { 155 | case e: Throwable => Failure(e) 156 | } 157 | res pipeTo sender() 158 | 159 | case FindLastFiddle(id) => 160 | val res = db 161 | .run(dal.findFiddleVersions(id)) 162 | .map { 163 | case fiddles if fiddles.nonEmpty => Success(fiddles.last) 164 | case _ => Failure(new NoSuchElementException) 165 | } 166 | .recover { 167 | case e: Throwable => Failure(e) 168 | } 169 | res pipeTo sender() 170 | 171 | case RemoveFiddle(id, version) => 172 | val res = db 173 | .run(dal.removeEvent(id, version)) 174 | .map { 175 | case 1 => Success(id) 176 | case _ => Failure(new NoSuchElementException) 177 | } 178 | .recover { 179 | case e: Throwable => Failure(e) 180 | } 181 | res pipeTo sender() 182 | 183 | case GetFiddleInfo => 184 | val res = db 185 | .run(dal.getAllEvents) 186 | .map(r => 187 | Success(r.map { 188 | case (id, version, name) => FiddleInfo(name, FiddleId(id, version)) 189 | }) 190 | ) 191 | .recover { 192 | case e: Throwable => Failure(e) 193 | } 194 | res pipeTo sender() 195 | 196 | case FindUser(id) => 197 | val res = db 198 | .run(dal.findUser(id)) 199 | .map { 200 | case Some(user) => Success(user) 201 | case None => Failure(new NoSuchElementException) 202 | } 203 | .recover { 204 | case e: Throwable => Failure(e) 205 | } 206 | res pipeTo sender() 207 | 208 | case FindUserLogin(loginInfo) => 209 | val res = db 210 | .run(dal.findUser(loginInfo)) 211 | .map { 212 | case Some(user) => Success(user) 213 | case None => Failure(new NoSuchElementException) 214 | } 215 | .recover { 216 | case e: Throwable => Failure(e) 217 | } 218 | res pipeTo sender() 219 | 220 | case AddUser(user) => 221 | db.run(dal.findUser(user.loginInfo)) 222 | .flatMap { 223 | case Some(u) => 224 | // do not add the same user again 225 | Future.successful(Success(u)) 226 | case None => 227 | runAndReply(dal.insert(user))(_ => Success(user)) 228 | } pipeTo sender() 229 | 230 | case UpdateUser(user) => 231 | runAndReply(dal.update(user))(_ => Success(user)) pipeTo sender() 232 | 233 | case AddAccess(fiddleId, version, userId, embedded, sourceIP) => 234 | runAndReply( 235 | dal.insertAccess(FiddleAccess(0, fiddleId, version, System.currentTimeMillis(), userId, embedded, sourceIP)) 236 | )(_ => Success(fiddleId)) pipeTo sender() 237 | 238 | case FindAccesses(fiddleId) => 239 | runAndReply(dal.findAccesses(fiddleId))(Success(_)) pipeTo sender() 240 | 241 | case FindAccessesBetween(startTime, endTime) => 242 | runAndReply(dal.findAccesses(startTime, endTime))(Success(_)) pipeTo sender() 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /server/src/main/scala/scalafiddle/server/dao/AccessDAO.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.server.dao 2 | 3 | case class FiddleAccess( 4 | id: Int, 5 | fiddleId: String, 6 | version: Int, 7 | timeStamp: Long, 8 | userId: Option[String], 9 | embedded: Boolean, 10 | sourceIP: String 11 | ) 12 | 13 | trait AccessDAO { 14 | self: DriverComponent => 15 | 16 | import driver.api._ 17 | 18 | class Accesses(tag: Tag) extends Table[FiddleAccess](tag, "access") { 19 | def id = column[Int]("id", O.PrimaryKey, O.AutoInc) 20 | def fiddleId = column[String]("fiddle_id") 21 | def version = column[Int]("version") 22 | def timeStamp = column[Long]("timestamp") 23 | def userId = column[Option[String]]("user_id") 24 | def embedded = column[Boolean]("embedded") 25 | def sourceIP = column[String]("source_ip") 26 | 27 | def * = (id, fiddleId, version, timeStamp, userId, embedded, sourceIP) <> (FiddleAccess.tupled, FiddleAccess.unapply) 28 | } 29 | 30 | val accesses = TableQuery[Accesses] 31 | 32 | def insertAccess(access: FiddleAccess) = 33 | accesses += access 34 | 35 | def findAccesses(fiddleId: String) = 36 | accesses.filter(_.fiddleId === fiddleId).result 37 | 38 | def findAccesses(startTime: Long, endTime: Long) = 39 | accesses.filter(r => r.timeStamp < endTime && r.timeStamp >= startTime).result 40 | } 41 | -------------------------------------------------------------------------------- /server/src/main/scala/scalafiddle/server/dao/DriverComponent.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.server.dao 2 | 3 | import slick.jdbc.JdbcProfile 4 | 5 | trait DriverComponent { 6 | val driver: JdbcProfile 7 | } 8 | -------------------------------------------------------------------------------- /server/src/main/scala/scalafiddle/server/dao/FiddleDAL.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.server.dao 2 | 3 | import slick.jdbc.JdbcProfile 4 | 5 | class FiddleDAL(val driver: JdbcProfile) extends FiddleDAO with UserDAO with AccessDAO with DriverComponent 6 | -------------------------------------------------------------------------------- /server/src/main/scala/scalafiddle/server/dao/FiddleDAO.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.server.dao 2 | 3 | case class Fiddle( 4 | id: String, 5 | version: Int, 6 | name: String, 7 | description: String, 8 | sourceCode: String, 9 | libraries: List[String], 10 | scalaVersion: String, 11 | scalaJSVersion: String, 12 | user: String, 13 | parent: Option[String] = None, 14 | created: Long = System.currentTimeMillis(), 15 | removed: Boolean = false 16 | ) 17 | 18 | trait FiddleDAO { self: DriverComponent => 19 | 20 | import driver.api._ 21 | 22 | implicit val stringListMapper = MappedColumnType.base[List[String], String]( 23 | list => list.mkString("\n"), 24 | string => string.split('\n').toList 25 | ) 26 | 27 | class Fiddles(tag: Tag) extends Table[Fiddle](tag, "fiddle") { 28 | def id = column[String]("id") 29 | def version = column[Int]("version") 30 | def name = column[String]("name") 31 | def description = column[String]("description") 32 | def sourceCode = column[String]("sourcecode") 33 | def libraries = column[List[String]]("libraries") 34 | def scalaVersion = column[String]("scala_version") 35 | def scalaJSVersion = column[String]("scalajs_version") 36 | def user = column[String]("user") 37 | def parent = column[Option[String]]("parent") 38 | def created = column[Long]("created") 39 | def removed = column[Boolean]("removed") 40 | def pk = primaryKey("pk_fiddle", (id, version)) 41 | 42 | def * = 43 | (id, version, name, description, sourceCode, libraries, scalaVersion, scalaJSVersion, user, parent, created, removed) <> (Fiddle.tupled, Fiddle.unapply) 44 | } 45 | 46 | val fiddles = TableQuery[Fiddles] 47 | 48 | def insertFiddle(fiddle: Fiddle) = 49 | fiddles += fiddle 50 | 51 | def findFiddle(id: String, version: Int) = 52 | fiddles.filter(f => f.id === id && f.version === version && f.removed === false).result.headOption 53 | 54 | def findFiddleVersions(id: String) = 55 | fiddles.filter(_.id === id).sortBy(_.version).result 56 | 57 | def findUserFiddles(userId: String) = 58 | fiddles.filter(_.user === userId).result 59 | 60 | def removeEvent(id: String, version: Int) = 61 | fiddles.filter(f => f.id === id && f.version === version).map(_.removed).update(true) 62 | 63 | def getAllEvents = 64 | fiddles.filter(_.name =!= "").map(f => (f.id, f.version, f.name)).result 65 | } 66 | -------------------------------------------------------------------------------- /server/src/main/scala/scalafiddle/server/dao/UserDAO.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.server.dao 2 | 3 | import com.mohiva.play.silhouette.api.LoginInfo 4 | 5 | import scalafiddle.server.models.User 6 | 7 | trait UserDAO { self: DriverComponent => 8 | 9 | import driver.api._ 10 | 11 | implicit val loginInfoMapper = MappedColumnType.base[LoginInfo, String]( 12 | info => info.providerID + "\n" + info.providerKey, 13 | string => { 14 | val parts = string.split('\n') 15 | LoginInfo(parts(0), parts(1)) 16 | } 17 | ) 18 | 19 | class Users(tag: Tag) extends Table[User](tag, "user") { 20 | def userID = column[String]("user_id", O.PrimaryKey) 21 | def loginInfo = column[LoginInfo]("login_info") 22 | def firstName = column[Option[String]]("first_name") 23 | def lastName = column[Option[String]]("last_name") 24 | def fullName = column[Option[String]]("full_name") 25 | def email = column[Option[String]]("email") 26 | def avatarURL = column[Option[String]]("avatar_url") 27 | def activated = column[Boolean]("activated") 28 | 29 | def * = (userID, loginInfo, firstName, lastName, fullName, email, avatarURL, activated) <> (User.tupled, User.unapply) 30 | } 31 | 32 | val users = TableQuery[Users] 33 | 34 | def insert(user: User) = 35 | users += user 36 | 37 | def update(user: User) = 38 | users.filter(_.userID === user.userID).update(user) 39 | 40 | def findUser(id: String) = 41 | users.filter(_.userID === id).result.headOption 42 | 43 | def findUser(loginInfo: LoginInfo) = 44 | users.filter(_.loginInfo === loginInfo).result.headOption 45 | } 46 | -------------------------------------------------------------------------------- /server/src/main/scala/scalafiddle/server/models/AuthToken.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.server.models 2 | 3 | import java.util.UUID 4 | 5 | import org.joda.time.DateTime 6 | 7 | /** 8 | * A token to authenticate a user against an endpoint for a short time period. 9 | * 10 | * @param id The unique token ID. 11 | * @param userID The unique ID of the user the token is associated with. 12 | * @param expiry The date-time the token expires. 13 | */ 14 | case class AuthToken(id: UUID, userID: UUID, expiry: DateTime) 15 | -------------------------------------------------------------------------------- /server/src/main/scala/scalafiddle/server/models/User.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.server.models 2 | 3 | import com.mohiva.play.silhouette.api.{Identity, LoginInfo} 4 | 5 | /** 6 | * The user object. 7 | * 8 | * @param userID The unique ID of the user. 9 | * @param loginInfo The linked login info. 10 | * @param firstName Maybe the first name of the authenticated user. 11 | * @param lastName Maybe the last name of the authenticated user. 12 | * @param fullName Maybe the full name of the authenticated user. 13 | * @param email Maybe the email of the authenticated provider. 14 | * @param avatarURL Maybe the avatar URL of the authenticated provider. 15 | * @param activated Indicates that the user has activated its registration. 16 | */ 17 | case class User( 18 | userID: String, 19 | loginInfo: LoginInfo, 20 | firstName: Option[String], 21 | lastName: Option[String], 22 | fullName: Option[String], 23 | email: Option[String], 24 | avatarURL: Option[String], 25 | activated: Boolean 26 | ) extends Identity { 27 | 28 | /** 29 | * Tries to construct a name. 30 | * 31 | * @return Maybe a name. 32 | */ 33 | def name = fullName.orElse { 34 | firstName -> lastName match { 35 | case (Some(f), Some(l)) => Some(f + " " + l) 36 | case (Some(f), None) => Some(f) 37 | case (None, Some(l)) => Some(l) 38 | case _ => None 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /server/src/main/scala/scalafiddle/server/models/services/UserService.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.server.models.services 2 | 3 | import java.util.UUID 4 | 5 | import com.mohiva.play.silhouette.api.services.IdentityService 6 | import com.mohiva.play.silhouette.impl.providers.CommonSocialProfile 7 | 8 | import scala.concurrent.Future 9 | import scalafiddle.server.models.User 10 | 11 | /** 12 | * Handles actions to users. 13 | */ 14 | trait UserService extends IdentityService[User] { 15 | 16 | /** 17 | * Retrieves a user that matches the specified ID. 18 | * 19 | * @param id The ID to retrieve a user. 20 | * @return The retrieved user or None if no user could be retrieved for the given ID. 21 | */ 22 | def retrieve(id: UUID): Future[Option[User]] 23 | 24 | /** 25 | * Saves a user. 26 | * 27 | * @param user The user to save. 28 | * @return The saved user. 29 | */ 30 | def save(user: User): Future[User] 31 | 32 | /** 33 | * Saves the social profile for a user. 34 | * 35 | * If a user exists for this profile then update the user, otherwise create a new user with the given profile. 36 | * 37 | * @param profile The social profile to save. 38 | * @return The user for whom the profile was saved. 39 | */ 40 | def save(profile: CommonSocialProfile): Future[User] 41 | } 42 | -------------------------------------------------------------------------------- /server/src/main/scala/scalafiddle/server/models/services/UserServiceImpl.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.server.models.services 2 | import java.util.UUID 3 | import javax.inject.{Inject, Named} 4 | 5 | import akka.actor._ 6 | import akka.pattern.ask 7 | import akka.util.Timeout 8 | import com.mohiva.play.silhouette.api.LoginInfo 9 | import com.mohiva.play.silhouette.impl.providers.CommonSocialProfile 10 | import play.api.Logger 11 | 12 | import scala.concurrent.ExecutionContext.Implicits.global 13 | import scala.concurrent.Future 14 | import scala.concurrent.duration._ 15 | import scala.util.{Success, Try} 16 | import scalafiddle.server._ 17 | import scalafiddle.server.models.User 18 | 19 | class UserServiceImpl @Inject() (@Named("persistence") persistence: ActorRef) extends UserService { 20 | implicit val timeout = Timeout(15.seconds) 21 | val log = Logger(getClass) 22 | 23 | /** 24 | * Retrieves a user that matches the specified ID. 25 | * 26 | * @param id The ID to retrieve a user. 27 | * @return The retrieved user or None if no user could be retrieved for the given ID. 28 | */ 29 | override def retrieve(id: UUID): Future[Option[User]] = { 30 | ask(persistence, FindUser(id.toString)).mapTo[Try[User]].map { 31 | case Success(user) => 32 | Some(user) 33 | case _ => 34 | None 35 | } 36 | } 37 | 38 | /** 39 | * Saves a user. 40 | * 41 | * @param user The user to save. 42 | * @return The saved user. 43 | */ 44 | override def save(user: User): Future[User] = { 45 | ask(persistence, AddUser(user)).mapTo[Try[User]].map { 46 | case Success(u) => 47 | u 48 | case _ => 49 | throw new Exception("Unable to save user") 50 | } 51 | } 52 | 53 | /** 54 | * Saves the social profile for a user. 55 | * 56 | * If a user exists for this profile then update the user, otherwise create a new user with the given profile. 57 | * 58 | * @param profile The social profile to save. 59 | * @return The user for whom the profile was saved. 60 | */ 61 | override def save(profile: CommonSocialProfile): Future[User] = { 62 | log.debug(s"User $profile logged in") 63 | val user = User( 64 | UUID.randomUUID().toString, 65 | profile.loginInfo, 66 | profile.firstName, 67 | profile.lastName, 68 | profile.fullName, 69 | profile.email, 70 | profile.avatarURL, 71 | true 72 | ) 73 | ask(persistence, AddUser(user)).mapTo[Try[User]].map { 74 | case Success(u) => 75 | u 76 | case _ => 77 | throw new Exception("Unable to save user") 78 | } 79 | } 80 | 81 | override def retrieve(loginInfo: LoginInfo): Future[Option[User]] = { 82 | ask(persistence, FindUserLogin(loginInfo)).mapTo[Try[User]].map { 83 | case Success(user) => 84 | Some(user) 85 | case _ => 86 | None 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /server/src/main/scala/scalafiddle/server/modules/SilhouetteModule.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.server.modules 2 | 3 | import com.google.inject.name.Named 4 | import com.google.inject.{AbstractModule, Provides} 5 | import com.mohiva.play.silhouette.api.crypto._ 6 | import com.mohiva.play.silhouette.api.services._ 7 | import com.mohiva.play.silhouette.api.util._ 8 | import com.mohiva.play.silhouette.api.{Environment, EventBus, Silhouette, SilhouetteProvider} 9 | import com.mohiva.play.silhouette.crypto.{JcaCrypter, JcaCrypterSettings, JcaSigner, JcaSignerSettings} 10 | import com.mohiva.play.silhouette.impl.authenticators._ 11 | import com.mohiva.play.silhouette.impl.providers._ 12 | import com.mohiva.play.silhouette.impl.providers.oauth1._ 13 | import com.mohiva.play.silhouette.impl.providers.oauth1.secrets.{CookieSecretProvider, CookieSecretSettings} 14 | import com.mohiva.play.silhouette.impl.providers.oauth1.services.PlayOAuth1Service 15 | import com.mohiva.play.silhouette.impl.providers.oauth2._ 16 | import com.mohiva.play.silhouette.impl.providers.openid.YahooProvider 17 | import com.mohiva.play.silhouette.impl.providers.openid.services.PlayOpenIDService 18 | import com.mohiva.play.silhouette.impl.providers.state.{CsrfStateItemHandler, CsrfStateSettings} 19 | import com.mohiva.play.silhouette.impl.services._ 20 | import com.mohiva.play.silhouette.impl.util._ 21 | import com.mohiva.play.silhouette.password.BCryptPasswordHasher 22 | import net.ceedubs.ficus.Ficus._ 23 | import net.ceedubs.ficus.readers.ArbitraryTypeReader._ 24 | import net.codingwell.scalaguice.ScalaModule 25 | import play.api.Configuration 26 | import play.api.libs.concurrent.AkkaGuiceSupport 27 | import play.api.libs.openid.OpenIdClient 28 | import play.api.libs.ws.WSClient 29 | import play.api.mvc.CookieHeaderEncoding 30 | 31 | import scalafiddle.server.Persistence 32 | import scalafiddle.server.models.services.{UserService, UserServiceImpl} 33 | import scalafiddle.server.utils.auth.DefaultEnv 34 | 35 | import scala.concurrent.ExecutionContext.Implicits.global 36 | 37 | /** 38 | * The Guice module which wires all Silhouette dependencies. 39 | */ 40 | class SilhouetteModule extends AbstractModule with ScalaModule with AkkaGuiceSupport { 41 | 42 | /** 43 | * Configures the module. 44 | */ 45 | def configure() { 46 | bind[Silhouette[DefaultEnv]].to[SilhouetteProvider[DefaultEnv]] 47 | bind[UserService].to[UserServiceImpl] 48 | bind[CacheLayer].to[PlayCacheLayer] 49 | bind[IDGenerator].toInstance(new SecureRandomIDGenerator()) 50 | bind[PasswordHasher].toInstance(new BCryptPasswordHasher) 51 | bind[FingerprintGenerator].toInstance(new DefaultFingerprintGenerator(false)) 52 | bind[EventBus].toInstance(EventBus()) 53 | bind[Clock].toInstance(Clock()) 54 | bindActor[Persistence]("persistence") 55 | } 56 | 57 | /** 58 | * Provides the HTTP layer implementation. 59 | * 60 | * @param client Play's WS client. 61 | * @return The HTTP layer implementation. 62 | */ 63 | @Provides 64 | def provideHTTPLayer(client: WSClient): HTTPLayer = new PlayHTTPLayer(client) 65 | 66 | /** 67 | * Provides the Silhouette environment. 68 | * 69 | * @param userService The user service implementation. 70 | * @param authenticatorService The authentication service implementation. 71 | * @param eventBus The event bus instance. 72 | * @return The Silhouette environment. 73 | */ 74 | @Provides 75 | def provideEnvironment( 76 | userService: UserService, 77 | authenticatorService: AuthenticatorService[CookieAuthenticator], 78 | eventBus: EventBus 79 | ): Environment[DefaultEnv] = { 80 | 81 | Environment[DefaultEnv]( 82 | userService, 83 | authenticatorService, 84 | Seq(), 85 | eventBus 86 | ) 87 | } 88 | 89 | /** 90 | * Provides the social provider registry. 91 | * 92 | * @param facebookProvider The Facebook provider implementation. 93 | * @param googleProvider The Google provider implementation. 94 | * @param vkProvider The VK provider implementation. 95 | * @param twitterProvider The Twitter provider implementation. 96 | * @param xingProvider The Xing provider implementation. 97 | * @param yahooProvider The Yahoo provider implementation. 98 | * @return The Silhouette environment. 99 | */ 100 | @Provides 101 | def provideSocialProviderRegistry( 102 | facebookProvider: FacebookProvider, 103 | googleProvider: GoogleProvider, 104 | githubProvider: GitHubProvider, 105 | gitlabProvider: GitLabProvider, 106 | vkProvider: VKProvider, 107 | twitterProvider: TwitterProvider, 108 | xingProvider: XingProvider, 109 | yahooProvider: YahooProvider 110 | ): SocialProviderRegistry = { 111 | 112 | SocialProviderRegistry( 113 | Seq( 114 | googleProvider, 115 | facebookProvider, 116 | twitterProvider, 117 | vkProvider, 118 | xingProvider, 119 | yahooProvider, 120 | githubProvider, 121 | gitlabProvider 122 | ) 123 | ) 124 | } 125 | 126 | /** 127 | * Provides the cookie signer for the OAuth1 token secret provider. 128 | * 129 | * @param configuration The Play configuration. 130 | * @return The cookie signer for the OAuth1 token secret provider. 131 | */ 132 | @Provides 133 | @Named("oauth1-token-secret-cookie-signer") 134 | def provideOAuth1TokenSecretCookieSigner(configuration: Configuration): Signer = { 135 | val config = configuration.underlying.as[JcaSignerSettings]("silhouette.oauth1TokenSecretProvider.cookie.signer") 136 | 137 | new JcaSigner(config) 138 | } 139 | 140 | /** 141 | * Provides the crypter for the OAuth1 token secret provider. 142 | * 143 | * @param configuration The Play configuration. 144 | * @return The crypter for the OAuth1 token secret provider. 145 | */ 146 | @Provides 147 | @Named("oauth1-token-secret-crypter") 148 | def provideOAuth1TokenSecretCrypter(configuration: Configuration): Crypter = { 149 | val config = configuration.underlying.as[JcaCrypterSettings]("silhouette.oauth1TokenSecretProvider.crypter") 150 | 151 | new JcaCrypter(config) 152 | } 153 | 154 | /** 155 | * Provides the signer for the CSRF state item handler. 156 | * 157 | * @param configuration The Play configuration. 158 | * @return The signer for the CSRF state item handler. 159 | */ 160 | @Provides @Named("csrf-state-item-signer") 161 | def provideCSRFStateItemSigner(configuration: Configuration): Signer = { 162 | val config = configuration.underlying.as[JcaSignerSettings]("silhouette.csrfStateItemHandler.signer") 163 | 164 | new JcaSigner(config) 165 | } 166 | 167 | /** 168 | * Provides the cookie signer for the authenticator. 169 | * 170 | * @param configuration The Play configuration. 171 | * @return The cookie signer for the authenticator. 172 | */ 173 | @Provides 174 | @Named("authenticator-signer") 175 | def provideAuthenticatorSigner(configuration: Configuration): Signer = { 176 | val config = configuration.underlying.as[JcaSignerSettings]("silhouette.authenticator.signer") 177 | 178 | new JcaSigner(config) 179 | } 180 | 181 | /** 182 | * Provides the crypter for the authenticator. 183 | * 184 | * @param configuration The Play configuration. 185 | * @return The crypter for the authenticator. 186 | */ 187 | @Provides 188 | @Named("authenticator-crypter") 189 | def provideAuthenticatorCrypter(configuration: Configuration): Crypter = { 190 | val config = configuration.underlying.as[JcaCrypterSettings]("silhouette.authenticator.crypter") 191 | 192 | new JcaCrypter(config) 193 | } 194 | 195 | /** 196 | * Provides the authenticator service. 197 | * 198 | * @param cookieSigner The cookie signer implementation. 199 | * @param crypter The crypter implementation. 200 | * @param fingerprintGenerator The fingerprint generator implementation. 201 | * @param idGenerator The ID generator implementation. 202 | * @param configuration The Play configuration. 203 | * @param clock The clock instance. 204 | * @return The authenticator service. 205 | */ 206 | @Provides 207 | def provideAuthenticatorService( 208 | @Named("authenticator-signer") cookieSigner: Signer, 209 | @Named("authenticator-crypter") crypter: Crypter, 210 | cookieHeaderEncoding: CookieHeaderEncoding, 211 | fingerprintGenerator: FingerprintGenerator, 212 | idGenerator: IDGenerator, 213 | configuration: Configuration, 214 | clock: Clock 215 | ): AuthenticatorService[CookieAuthenticator] = { 216 | 217 | val config = configuration.underlying.as[CookieAuthenticatorSettings]("silhouette.authenticator") 218 | val encoder = new CrypterAuthenticatorEncoder(crypter) 219 | 220 | new CookieAuthenticatorService( 221 | config, 222 | None, 223 | cookieSigner, 224 | cookieHeaderEncoding, 225 | encoder, 226 | fingerprintGenerator, 227 | idGenerator, 228 | clock 229 | ) 230 | } 231 | 232 | /** 233 | * Provides the avatar service. 234 | * 235 | * @param httpLayer The HTTP layer implementation. 236 | * @return The avatar service implementation. 237 | */ 238 | @Provides 239 | def provideAvatarService(httpLayer: HTTPLayer): AvatarService = new GravatarService(httpLayer) 240 | 241 | /** 242 | * Provides the OAuth1 token secret provider. 243 | * 244 | * @param cookieSigner The cookie signer implementation. 245 | * @param crypter The crypter implementation. 246 | * @param configuration The Play configuration. 247 | * @param clock The clock instance. 248 | * @return The OAuth1 token secret provider implementation. 249 | */ 250 | @Provides 251 | def provideOAuth1TokenSecretProvider( 252 | @Named("oauth1-token-secret-cookie-signer") cookieSigner: Signer, 253 | @Named("oauth1-token-secret-crypter") crypter: Crypter, 254 | configuration: Configuration, 255 | clock: Clock 256 | ): OAuth1TokenSecretProvider = { 257 | 258 | val settings = configuration.underlying.as[CookieSecretSettings]("silhouette.oauth1TokenSecretProvider") 259 | new CookieSecretProvider(settings, cookieSigner, crypter, clock) 260 | } 261 | 262 | /** 263 | * Provides the password hasher registry. 264 | * 265 | * @param passwordHasher The default password hasher implementation. 266 | * @return The password hasher registry. 267 | */ 268 | @Provides 269 | def providePasswordHasherRegistry(passwordHasher: PasswordHasher): PasswordHasherRegistry = { 270 | new PasswordHasherRegistry(passwordHasher) 271 | } 272 | 273 | /** 274 | * Provides the CSRF state item handler. 275 | * 276 | * @param idGenerator The ID generator implementation. 277 | * @param signer The signer implementation. 278 | * @param configuration The Play configuration. 279 | * @return The CSRF state item implementation. 280 | */ 281 | @Provides 282 | def provideCsrfStateItemHandler( 283 | idGenerator: IDGenerator, 284 | @Named("csrf-state-item-signer") signer: Signer, 285 | configuration: Configuration 286 | ): CsrfStateItemHandler = { 287 | val settings = configuration.underlying.as[CsrfStateSettings]("silhouette.csrfStateItemHandler") 288 | new CsrfStateItemHandler(settings, idGenerator, signer) 289 | } 290 | 291 | /** 292 | * Provides the signer for the social state handler. 293 | * 294 | * @param configuration The Play configuration. 295 | * @return The signer for the social state handler. 296 | */ 297 | @Provides @Named("social-state-signer") 298 | def provideSocialStateSigner(configuration: Configuration): Signer = { 299 | val config = configuration.underlying.as[JcaSignerSettings]("silhouette.socialStateHandler.signer") 300 | 301 | new JcaSigner(config) 302 | } 303 | 304 | /** 305 | * Provides the social state handler. 306 | * 307 | * @param signer The signer implementation. 308 | * @return The social state handler implementation. 309 | */ 310 | @Provides 311 | def provideSocialStateHandler( 312 | @Named("social-state-signer") signer: Signer, 313 | csrfStateItemHandler: CsrfStateItemHandler 314 | ): SocialStateHandler = { 315 | 316 | new DefaultSocialStateHandler(Set(csrfStateItemHandler), signer) 317 | } 318 | 319 | /** 320 | * Provides the Github provider. 321 | * 322 | * @param httpLayer The HTTP layer implementation. 323 | * @param socialStateHandler The OAuth2 state provider implementation. 324 | * @param configuration The Play configuration. 325 | * @return The Github provider. 326 | */ 327 | @Provides 328 | def provideGitHubProvider( 329 | httpLayer: HTTPLayer, 330 | socialStateHandler: SocialStateHandler, 331 | configuration: Configuration 332 | ): GitHubProvider = { 333 | 334 | new GitHubProvider(httpLayer, socialStateHandler, configuration.underlying.as[OAuth2Settings]("silhouette.github")) 335 | } 336 | 337 | /** 338 | * Provides the Github provider. 339 | * 340 | * @param httpLayer The HTTP layer implementation. 341 | * @param socialStateHandler The OAuth2 state provider implementation. 342 | * @param configuration The Play configuration. 343 | * @return The Github provider. 344 | */ 345 | @Provides 346 | def provideGitLabProvider( 347 | httpLayer: HTTPLayer, 348 | socialStateHandler: SocialStateHandler, 349 | configuration: Configuration 350 | ): GitLabProvider = { 351 | 352 | new GitLabProvider(httpLayer, socialStateHandler, configuration.underlying.as[OAuth2Settings]("silhouette.gitlab")) 353 | } 354 | 355 | /** 356 | * Provides the Facebook provider. 357 | * 358 | * @param httpLayer The HTTP layer implementation. 359 | * @param socialStateHandler The social state handler implementation. 360 | * @param configuration The Play configuration. 361 | * @return The Facebook provider. 362 | */ 363 | @Provides 364 | def provideFacebookProvider( 365 | httpLayer: HTTPLayer, 366 | socialStateHandler: SocialStateHandler, 367 | configuration: Configuration 368 | ): FacebookProvider = { 369 | 370 | new FacebookProvider(httpLayer, socialStateHandler, configuration.underlying.as[OAuth2Settings]("silhouette.facebook")) 371 | } 372 | 373 | /** 374 | * Provides the Google provider. 375 | * 376 | * @param httpLayer The HTTP layer implementation. 377 | * @param socialStateHandler The social state handler implementation. 378 | * @param configuration The Play configuration. 379 | * @return The Google provider. 380 | */ 381 | @Provides 382 | def provideGoogleProvider( 383 | httpLayer: HTTPLayer, 384 | socialStateHandler: SocialStateHandler, 385 | configuration: Configuration 386 | ): GoogleProvider = { 387 | 388 | new GoogleProvider(httpLayer, socialStateHandler, configuration.underlying.as[OAuth2Settings]("silhouette.google")) 389 | } 390 | 391 | /** 392 | * Provides the VK provider. 393 | * 394 | * @param httpLayer The HTTP layer implementation. 395 | * @param socialStateHandler The social state handler implementation. 396 | * @param configuration The Play configuration. 397 | * @return The VK provider. 398 | */ 399 | @Provides 400 | def provideVKProvider( 401 | httpLayer: HTTPLayer, 402 | socialStateHandler: SocialStateHandler, 403 | configuration: Configuration 404 | ): VKProvider = { 405 | 406 | new VKProvider(httpLayer, socialStateHandler, configuration.underlying.as[OAuth2Settings]("silhouette.vk")) 407 | } 408 | 409 | /** 410 | * Provides the Twitter provider. 411 | * 412 | * @param httpLayer The HTTP layer implementation. 413 | * @param tokenSecretProvider The token secret provider implementation. 414 | * @param configuration The Play configuration. 415 | * @return The Twitter provider. 416 | */ 417 | @Provides 418 | def provideTwitterProvider( 419 | httpLayer: HTTPLayer, 420 | tokenSecretProvider: OAuth1TokenSecretProvider, 421 | configuration: Configuration 422 | ): TwitterProvider = { 423 | 424 | val settings = configuration.underlying.as[OAuth1Settings]("silhouette.twitter") 425 | new TwitterProvider(httpLayer, new PlayOAuth1Service(settings), tokenSecretProvider, settings) 426 | } 427 | 428 | /** 429 | * Provides the Xing provider. 430 | * 431 | * @param httpLayer The HTTP layer implementation. 432 | * @param tokenSecretProvider The token secret provider implementation. 433 | * @param configuration The Play configuration. 434 | * @return The Xing provider. 435 | */ 436 | @Provides 437 | def provideXingProvider( 438 | httpLayer: HTTPLayer, 439 | tokenSecretProvider: OAuth1TokenSecretProvider, 440 | configuration: Configuration 441 | ): XingProvider = { 442 | 443 | val settings = configuration.underlying.as[OAuth1Settings]("silhouette.xing") 444 | new XingProvider(httpLayer, new PlayOAuth1Service(settings), tokenSecretProvider, settings) 445 | } 446 | 447 | /** 448 | * Provides the Yahoo provider. 449 | * 450 | * @param httpLayer The HTTP layer implementation. 451 | * @param client The OpenID client implementation. 452 | * @param configuration The Play configuration. 453 | * @return The Yahoo provider. 454 | */ 455 | @Provides 456 | def provideYahooProvider(httpLayer: HTTPLayer, client: OpenIdClient, configuration: Configuration): YahooProvider = { 457 | 458 | val settings = configuration.underlying.as[OpenIDSettings]("silhouette.yahoo") 459 | new YahooProvider(httpLayer, new PlayOpenIDService(client, settings), settings) 460 | } 461 | } 462 | -------------------------------------------------------------------------------- /server/src/main/scala/scalafiddle/server/utils/auth/AllLoginProviders.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.server.utils.auth 2 | 3 | import scalafiddle.shared.LoginProvider 4 | 5 | object AllLoginProviders { 6 | val providers: Map[String, LoginProvider] = Map( 7 | "github" -> LoginProvider("github", "GitHub", "/assets/images/providers/github.png") 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /server/src/main/scala/scalafiddle/server/utils/auth/Env.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.server.utils.auth 2 | 3 | import com.mohiva.play.silhouette.api.Env 4 | import com.mohiva.play.silhouette.impl.authenticators.CookieAuthenticator 5 | 6 | import scalafiddle.server.models.User 7 | 8 | /** 9 | * The default env. 10 | */ 11 | trait DefaultEnv extends Env { 12 | type I = User 13 | type A = CookieAuthenticator 14 | } 15 | -------------------------------------------------------------------------------- /server/src/main/scala/scalafiddle/server/utils/auth/WithProvider.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.server.utils.auth 2 | 3 | import com.mohiva.play.silhouette.api.{Authenticator, Authorization} 4 | import play.api.mvc.Request 5 | 6 | import scala.concurrent.Future 7 | import scalafiddle.server.models.User 8 | 9 | /** 10 | * Grants only access if a user has authenticated with the given provider. 11 | * 12 | * @param provider The provider ID the user must authenticated with. 13 | * @tparam A The type of the authenticator. 14 | */ 15 | case class WithProvider[A <: Authenticator](provider: String) extends Authorization[User, A] { 16 | 17 | /** 18 | * Indicates if a user is authorized to access an action. 19 | * 20 | * @param user The usr object. 21 | * @param authenticator The authenticator instance. 22 | * @param request The current request. 23 | * @tparam B The type of the request body. 24 | * @return True if the user is authorized, false otherwise. 25 | */ 26 | override def isAuthorized[B](user: User, authenticator: A)(implicit request: Request[B]): Future[Boolean] = { 27 | 28 | Future.successful(user.loginInfo.providerID == provider) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/src/main/twirl/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String, fiddleData: String, fiddleId: Option[String])(implicit config: play.api.Configuration, env: play.api.Environment) 2 | 3 | 4 | 5 | 6 | 7 | 8 | @title 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | @if(fiddleId.isDefined) { 33 | 35 | } 36 | 37 | 38 | 39 |
40 | 50 | @scalajs.html.scripts(projectName = "client", fileName => routes.Assets.versioned(fileName).toString, name => getClass.getResource(s"/public/$name") != null) 51 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /server/src/main/twirl/views/listfiddles.scala.html: -------------------------------------------------------------------------------- 1 | @import scalafiddle.server.FiddleInfo 2 | @(fiddles: Seq[FiddleInfo]) 3 | 4 | 5 | 6 | 7 | 8 | 9 | ScalaFiddle listing 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 |
36 | 37 |
38 |
39 | 47 |
48 |
49 | 50 | 51 | -------------------------------------------------------------------------------- /server/src/main/twirl/views/resultframe.scala.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 |
13 | 85 |
86 | 87 | 88 | -------------------------------------------------------------------------------- /server/src/test/resources/test-embed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 |

Next

10 | 11 |

Next

12 | 16 | -------------------------------------------------------------------------------- /server/src/test/scala/scalafiddle/server/HighlighterSpec.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.server 2 | 3 | import org.scalatest._ 4 | 5 | class HighlighterSpec extends WordSpec with Matchers { 6 | "Highlighter" should { 7 | "highlight Scala code" in { 8 | val code = 9 | """ 10 | |object Highlighter { 11 | | sealed trait HighlightCode 12 | | 13 | | case object Comment extends HighlightCode 14 | | case object Type extends HighlightCode 15 | | case object Literal extends HighlightCode 16 | | case object Keyword extends HighlightCode 17 | | case object Reset extends HighlightCode 18 | | 19 | | object BackTicked { 20 | | private[this] val regex = "`([^`]+)`".r 21 | | def unapplySeq(s: Any): Option[List[String]] = { 22 | | regex.unapplySeq(s.toString) 23 | | } 24 | | } 25 | |} 26 | """.stripMargin 27 | 28 | val res = Highlighter.defaultHighlightIndices(code.toCharArray) 29 | println(res) 30 | } 31 | 32 | "highlight string interpolation" in { 33 | val code = 34 | """ 35 | |object Highlighter { 36 | | val x = 5 37 | | val y = s"Test ${x+2} thing" 38 | |} 39 | """.stripMargin 40 | val res = Highlighter.defaultHighlightIndices(code.toCharArray) 41 | println(res) 42 | } 43 | 44 | "highlight multiline string" in { 45 | val code = 46 | s""" 47 | |object A { 48 | | ${"\"\"\""} 49 | | Test 50 | | Test 51 | |${"\"\"\""} 52 | |} 53 | """.stripMargin 54 | val res = Highlighter.defaultHighlightIndices(code.toCharArray) 55 | println(res) 56 | } 57 | 58 | "highlight multiline comment" in { 59 | val code = 60 | s""" 61 | |object A { 62 | | /* 63 | | Test 64 | | Test 65 | | */ 66 | |} 67 | """.stripMargin 68 | val res = Highlighter.defaultHighlightIndices(code.toCharArray) 69 | println(res) 70 | } 71 | 72 | "highlight long text" in { 73 | val code = 74 | s"""|import fiddle.Fiddle, Fiddle.println 75 | |import scalajs.js 76 | | 77 | |@js.annotation.JSExportTopLevel("ScalaFiddle") 78 | |object ScalaFiddle { 79 | | // $$FiddleStart 80 | | // Start writing your ScalaFiddle code here 81 | |def libraryListing(scalaVersion: String) = Action { 82 | | val libStrings = librarian.libraries 83 | | .filter(_.scalaVersions.contains(scalaVersion)) 84 | | .flatMap(lib => Library.stringify(lib) +: lib.extraDeps) 85 | | Ok(write(libStrings)).as("application/json").withHeaders(CACHE_CONTROL -> "max-age=60") 86 | |} 87 | |def libraryListing(scalaVersion: String) = Action { 88 | | val libStrings = librarian.libraries 89 | | .filter(_.scalaVersions.contains(scalaVersion)) 90 | | .flatMap(lib => Library.stringify(lib) +: lib.extraDeps) 91 | | Ok(write(libStrings)).as("application/json").withHeaders(CACHE_CONTROL -> "max-age=60") 92 | |} 93 | |def libraryListing(scalaVersion: String) = Action { 94 | | val libStrings = librarian.libraries 95 | | .filter(_.scalaVersions.contains(scalaVersion)) 96 | | .flatMap(lib => Library.stringify(lib) +: lib.extraDeps) 97 | | Ok(write(libStrings)).as("application/json").withHeaders(CACHE_CONTROL -> "max-age=60") 98 | |} 99 | |def libraryListing(scalaVersion: String) = Action { 100 | | val libStrings = librarian.libraries 101 | | .filter(_.scalaVersions.contains(scalaVersion)) 102 | | .flatMap(lib => Library.stringify(lib) +: lib.extraDeps) 103 | | Ok(write(libStrings)).as("application/json").withHeaders(CACHE_CONTROL -> "max-age=60") 104 | |} 105 | |// $$FiddleEnd 106 | |} 107 | """.stripMargin 108 | val res = Highlighter.defaultHighlightIndices(code.toCharArray) 109 | println(res) 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /shared/src/main/scala/scalafiddle/shared/Api.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.shared 2 | 3 | import scala.concurrent.Future 4 | 5 | case class LoginProvider(id: String, name: String, logoUrl: String) 6 | 7 | case class UserInfo(id: String, name: String, avatarUrl: Option[String], loggedIn: Boolean) 8 | 9 | case class FiddleVersions(id: String, name: String, libraries: Seq[String], latestVersion: Int, updated: Long) 10 | 11 | trait Api { 12 | def save(fiddle: FiddleData): Future[Either[String, FiddleId]] 13 | 14 | def update(fiddle: FiddleData, id: String): Future[Either[String, FiddleId]] 15 | 16 | def fork(fiddle: FiddleData, id: String, version: Int): Future[Either[String, FiddleId]] 17 | 18 | def loginProviders(): Seq[LoginProvider] 19 | 20 | def userInfo(): UserInfo 21 | 22 | def listFiddles(): Future[Seq[FiddleVersions]] 23 | } 24 | -------------------------------------------------------------------------------- /shared/src/main/scala/scalafiddle/shared/FiddleData.scala: -------------------------------------------------------------------------------- 1 | package scalafiddle.shared 2 | 3 | case class FiddleId(id: String, version: Int) { 4 | override def toString: String = s"$id/$version" 5 | } 6 | 7 | case class Library( 8 | name: String, 9 | organization: String, 10 | artifact: String, 11 | version: String, 12 | compileTimeOnly: Boolean, 13 | scalaVersions: Seq[String], 14 | scalaJSVersions: Seq[String], 15 | extraDeps: Seq[String], 16 | group: String, 17 | docUrl: String, 18 | exampleUrl: Option[String] 19 | ) 20 | 21 | object Library { 22 | def stringify(lib: Library) = 23 | if (lib.compileTimeOnly) 24 | s"${lib.organization} %% ${lib.artifact} % ${lib.version}" 25 | else 26 | s"${lib.organization} %%% ${lib.artifact} % ${lib.version}" 27 | } 28 | 29 | case class FiddleData( 30 | name: String, 31 | description: String, 32 | sourceCode: String, 33 | libraries: Seq[Library], 34 | available: Seq[Library], 35 | scalaVersion: String, 36 | scalaJSVersion: String, 37 | author: Option[UserInfo], 38 | modified: Long 39 | ) 40 | --------------------------------------------------------------------------------