├── .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 | 
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 | 
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 | 
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 |
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 |
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 |
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 |
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 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/server/src/main/twirl/views/resultframe.scala.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
--------------------------------------------------------------------------------