diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ffb995b --- /dev/null +++ b/.gitignore @@ -0,0 +1,144 @@ +# Created by https://www.gitignore.io/api/scala,intellij,eclipse,sbt + +### Scala ### +*.class +*.log + +# sbt specific +.cache +.cache-main +.history +.lib/ +dist/* +target/ +lib_managed/ +src_managed/ +project/boot/ +project/plugins/project/ +project/target +project/project + +# Scala-IDE specific +.scala_dependencies +.worksheet + +# Documentation intermediate files +docs/src/main/paradox +docs/src/main/mdoc/api + + +### SublimeText ### +doodle.sublime-project +# cache files for sublime text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# workspace files are user-specific +*.sublime-workspace + +# project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using SublimeText +# *.sublime-project + +# sftp configuration file +sftp-config.json + + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio + +*.iml + +## Directory-based project format: +.idea/ +# if you remove the above rule, at least ignore the following: + +# User-specific stuff: +# .idea/workspace.xml +# .idea/tasks.xml +# .idea/dictionaries + +# Sensitive or high-churn files: +# .idea/dataSources.ids +# .idea/dataSources.xml +# .idea/sqlDataSources.xml +# .idea/dynamic.xml +# .idea/uiDesigner.xml + +# Gradle: +# .idea/gradle.xml +# .idea/libraries + +# Mongo Explorer plugin: +# .idea/mongoSettings.xml + +## File-based project format: +*.ipr +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties + + +### Eclipse ### +*.pydevproject +.metadata +.gradle +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath + +# Eclipse Core +.project + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# Java annotation processor (APT) +.factorypath + +# PDT-specific +.buildpath + +# sbteclipse plugin +.target + +# TeXlipse plugin +.texlipse + +# Metals and Bloop +.bsp +.metals +.bloop +project/metals.sbt + +.sbt-hydra-history diff --git a/.scalafix.conf b/.scalafix.conf new file mode 100644 index 0000000..4eedd04 --- /dev/null +++ b/.scalafix.conf @@ -0,0 +1,5 @@ +rules = [ + OrganizeImports +] +OrganizeImports.removeUnused = false +OrganizeImports.coalesceToWildcardImportThreshold = 5 diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..9b25812 --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,2 @@ +version = "3.7.2" +runner.dialect = scala3 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d9a10c0 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md new file mode 100644 index 0000000..072bfa2 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Gooey + +Build GUIs really quickly. Sweet! diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..1f3d257 --- /dev/null +++ b/build.sbt @@ -0,0 +1,151 @@ +/* + * Copyright 2015-2020 Creative Scala + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import scala.sys.process._ +import laika.rewrite.link.LinkConfig +import laika.rewrite.link.ApiLinks +import laika.theme.Theme + +ThisBuild / tlBaseVersion := "0.1" // your current series x.y + +ThisBuild / organization := "org.creativescala" +ThisBuild / organizationName := "Creative Scala" +ThisBuild / startYear := Some(2023) +ThisBuild / licenses := Seq(License.Apache2) +ThisBuild / developers := List( + // your GitHub handle and name + tlGitHubDev("noelwelsh", "Noel Welsh") +) + +// true by default, set to false to publish to s01.oss.sonatype.org +ThisBuild / tlSonatypeUseLegacyHost := true + +lazy val scala3 = "3.2.2" + +ThisBuild / crossScalaVersions := List(scala3) +ThisBuild / scalaVersion := crossScalaVersions.value.head +ThisBuild / useSuperShell := false +ThisBuild / scalafixDependencies += "com.github.liancheng" %% "organize-imports" % "0.6.0" +ThisBuild / semanticdbEnabled := true +ThisBuild / semanticdbVersion := scalafixSemanticdb.revision +ThisBuild / tlSitePublishBranch := Some("main") + +// Run this (build) to do everything involved in building the project +commands += Command.command("build") { state => + "dependencyUpdates" :: + "compile" :: + "test" :: + "scalafixAll" :: + "scalafmtAll" :: + state +} + +lazy val css = taskKey[Unit]("Build the CSS") + +lazy val commonSettings = Seq( + libraryDependencies ++= Seq( + Dependencies.munit.value + ) +) + +lazy val root = crossProject(JSPlatform, JVMPlatform) + .in(file(".")) + .settings(moduleName := "gooey") + .aggregate(core) + +lazy val core = crossProject(JSPlatform, JVMPlatform) + .in(file("core")) + .settings( + commonSettings, + libraryDependencies ++= Seq( + Dependencies.catsCore.value, + Dependencies.catsEffect.value + ), + moduleName := "gooey-core" + ) + +lazy val docs = + project + .in(file("docs")) + .settings( + laikaConfig := laikaConfig.value.withConfigValue( + LinkConfig(apiLinks = + Seq( + ApiLinks(baseUri = + "https://javadoc.io/doc/org.creativescala/gooey-docs_3/latest/" + ) + ) + ) + ), + mdocIn := file("docs/src/pages"), + css := { + val src = file("docs/src/css") + val dest1 = mdocOut.value + val dest2 = (laikaSite / target).value + val cmd1 = + s"npx tailwindcss -i ${src.toString}/creative-scala.css -o ${dest1.toString}/creative-scala.css" + val cmd2 = + s"npx tailwindcss -i ${src.toString}/creative-scala.css -o ${dest2.toString}/creative-scala.css" + cmd1 ! + + cmd2 ! + }, + Laika / sourceDirectories += file("docs/src/templates"), + laikaTheme := Theme.empty, + laikaExtensions ++= Seq( + laika.markdown.github.GitHubFlavor, + laika.parse.code.SyntaxHighlighting, + CreativeScalaDirectives + ), + tlSite := Def + .sequential( + (Compile / run).toTask(""), + mdoc.toTask(""), + css, + laikaSite + ) + .value + ) + .enablePlugins(TypelevelSitePlugin) + .dependsOn(core.jvm) + +lazy val unidocs = project + .in(file("unidocs")) + .enablePlugins(TypelevelUnidocPlugin) // also enables the ScalaUnidocPlugin + .settings( + name := "gooey-docs", + ScalaUnidoc / unidoc / unidocProjectFilter := + inAnyProject -- inProjects( + docs, + examples.js, + core.js + ) + ) + +// To avoid including this in the core build +lazy val examples = crossProject(JSPlatform, JVMPlatform) + .in(file("examples")) + .settings( + commonSettings, + moduleName := "gooey-examples" + ) + .jvmConfigure( + _.settings(mimaPreviousArtifacts := Set.empty) + .dependsOn(core.jvm) + ) + .jsConfigure( + _.settings(mimaPreviousArtifacts := Set.empty) + .dependsOn(core.js) + ) diff --git a/core/shared/src/main/scala/gooey/Checkbox.scala b/core/shared/src/main/scala/gooey/Checkbox.scala new file mode 100644 index 0000000..3bd65c7 --- /dev/null +++ b/core/shared/src/main/scala/gooey/Checkbox.scala @@ -0,0 +1,17 @@ +package gooey + +final case class Checkbox(label: Option[String], default: Boolean) + extends Component[Boolean], + Label[Checkbox] { + def withLabel(label: String): Checkbox = + this.copy(label = Some(label)) + + def withoutLabel: Checkbox = + this.copy(label = None) + + def withDefault(default: Boolean): Checkbox = + this.copy(default = default) +} +object Checkbox { + val empty: Checkbox = Checkbox(None, false) +} diff --git a/core/shared/src/main/scala/gooey/Component.scala b/core/shared/src/main/scala/gooey/Component.scala new file mode 100644 index 0000000..09b33ac --- /dev/null +++ b/core/shared/src/main/scala/gooey/Component.scala @@ -0,0 +1,6 @@ +package gooey + +/** A marker trait for UI components. Indicates that a component can produce a + * value of type `A`. By convention all components should extend this. + */ +trait Component[A] diff --git a/core/shared/src/main/scala/gooey/Interpreter.scala b/core/shared/src/main/scala/gooey/Interpreter.scala new file mode 100644 index 0000000..f391ea1 --- /dev/null +++ b/core/shared/src/main/scala/gooey/Interpreter.scala @@ -0,0 +1,8 @@ +package gooey + +/** An interpreter constructs a UI of type `Output` from a description with type + * given by `Input` + */ +trait Interpreter[Input[_], Output[_]] { + def build[A](in: Input[A]): Output[A] +} diff --git a/core/shared/src/main/scala/gooey/Labelable.scala b/core/shared/src/main/scala/gooey/Labelable.scala new file mode 100644 index 0000000..2d38ed2 --- /dev/null +++ b/core/shared/src/main/scala/gooey/Labelable.scala @@ -0,0 +1,8 @@ +package gooey + +/** API for components that can have an attached label. */ +trait Label[Self] { self: Self => + def label: Option[String] + def withLabel(label: String): Self + def withoutLabel: Self +} diff --git a/project/CreativeScalaDirectives.scala b/project/CreativeScalaDirectives.scala new file mode 100644 index 0000000..c1b80a3 --- /dev/null +++ b/project/CreativeScalaDirectives.scala @@ -0,0 +1,189 @@ +import cats.data._ +import cats.implicits._ +import laika.ast._ +import laika.directive._ + +object CreativeScalaDirectives extends DirectiveRegistry { + + override val description: String = + "Directive to work with CreativeScala SVG pictures." + + val divWithId: Blocks.Directive = + Blocks.create("divWithId") { + import Blocks.dsl._ + + attribute(0) + .as[String] + .map { (id) => + RawContent( + NonEmptySet.one("html"), + s"""
""" + ) + } + } + + // Parameters are id and then JS function to call + val doodle: Blocks.Directive = + Blocks.create("doodle") { + import Blocks.dsl._ + + (attribute(0).as[String], attribute(1).as[String], cursor) + .mapN { (id, js, _) => + BlockSequence( + Seq( + RawContent( + NonEmptySet.one("html"), + s"""""" + ), + RawContent( + NonEmptySet.one("html"), + s"""""" + ) + ) + ) + } + } + + // Insert a figure (image) + // + // Parameters: + // filename: String. The file name of the image + // key: String. The name of the reference for this image + // caption: String. The caption for this image + val figure: Blocks.Directive = + Blocks.create("figure") { + import Blocks.dsl._ + + ( + attribute("img").as[String].widen, + attribute("key").as[String].optional, + attribute("caption").as[String] + ).mapN { (img, key, caption) => + Paragraph( + Image( + Target.parse(img), + None, + None, + Some(caption), + Some(s"Figure $key: caption") + ) + ) + } + } + + val footnote: Blocks.Directive = + Blocks.create("footnote") { + import Blocks.dsl._ + + (attribute(0).as[String], parsedBody).mapN { (id, body) => + Footnote(id, body) + } + } + + // Insert a reference to a figure + // + // Parameters: + // key: String. The name of the figure being referred to. + val fref: Spans.Directive = + Spans.create("fref") { + import Spans.dsl._ + + (attribute(0).as[String]).map { (key) => Text(s"Figure $key") } + } + // + // Insert a reference to a footnote + // + // Parameters: + // key: String. The name of the footnote being referred to. + val fnref: Spans.Directive = + Spans.create("fnref") { + import Spans.dsl._ + + (attribute(0).as[String]).map { (key) => Text(s"Footnote $key") } + } + + val script: Blocks.Directive = + Blocks.create("script") { + import Blocks.dsl._ + + (attribute(0).as[String]).map { (js) => + RawContent(NonEmptySet.one("html"), s"") + } + } + + val solution: Blocks.Directive = + Blocks.create("solution") { + import Blocks.dsl._ + + parsedBody.map { body => + Section(Header(5, Text("Solution")), body) + } + } + + // Insert a reference to a table + // + // Parameters: + // key: String. The name of the figure being referred to. + val tref: Spans.Directive = + Spans.create("tref") { + import Spans.dsl._ + + (attribute(0).as[String]).map { (key) => Text(s"Table $key") } + } + + val compactNavBar: Templates.Directive = + Templates.create("compactNavBar") { + import Templates.dsl._ + + val leftArrow = "←" + val rightArrow = "→" + + cursor.map { cursor => + val previous = + cursor.flattenedSiblings.previousDocument + .map(c => SpanLink(Seq(Text(leftArrow)), InternalTarget(c.path))) + .getOrElse(Text("")) + val next = cursor.flattenedSiblings.nextDocument + .map(c => SpanLink(Seq(Text(rightArrow)), InternalTarget(c.path))) + .getOrElse(Text("")) + val here = cursor.root.target.title + .map(title => SpanLink(Seq(title), InternalTarget(Path.Root))) + .getOrElse(Text("")) + + TemplateElement( + BlockSequence( + Seq(Paragraph(previous), Paragraph(here), Paragraph(next)) + ) + ) + } + } + + val nextPage: Templates.Directive = + Templates.create("nextPage") { + import Templates.dsl._ + + val rightArrow = "→" + + cursor.map { cursor => + val next = cursor.flattenedSiblings.nextDocument + + val title = next.flatMap(c => c.target.title) + val path = next.map(c => c.path) + + val link = + (title, path).mapN { (t, p) => + Paragraph( + SpanLink(Seq(t, Text(rightArrow)), InternalTarget(p)) + ).withStyle("nextPage") + } + + TemplateElement(link.getOrElse(Text(""))) + } + } + + val spanDirectives = Seq(fref, fnref, tref) + val blockDirectives = + Seq(divWithId, doodle, figure, footnote, script, solution) + val templateDirectives = Seq(compactNavBar, nextPage) + val linkDirectives = Seq() +} diff --git a/project/Dependencies.scala b/project/Dependencies.scala new file mode 100644 index 0000000..eb38564 --- /dev/null +++ b/project/Dependencies.scala @@ -0,0 +1,33 @@ +import sbt._ +import org.scalajs.sbtplugin.ScalaJSPlugin +import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ +import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport._ + +object Dependencies { + // Library Versions + val catsVersion = "2.7.0" + val catsEffectVersion = "3.3.14" + val fs2Version = "3.1.1" + + val batikVersion = "1.16" + + val miniTestVersion = "2.9.6" + val scalaCheckVersion = "1.15.4" + val munitVersion = "0.7.29" + + // Libraries + val catsEffect = + Def.setting("org.typelevel" %%% "cats-effect" % catsEffectVersion) + val catsCore = Def.setting("org.typelevel" %%% "cats-core" % catsVersion) + val catsFree = Def.setting("org.typelevel" %%% "cats-free" % catsVersion) + val fs2 = Def.setting("co.fs2" %%% "fs2-core" % fs2Version) + + val batik = + Def.setting("org.apache.xmlgraphics" % "batik-transcoder" % batikVersion) + + val miniTest = + Def.setting("io.monix" %%% "minitest" % miniTestVersion % "test") + val miniTestLaws = + Def.setting("io.monix" %%% "minitest-laws" % miniTestVersion % "test") + val munit = Def.setting("org.scalameta" %% "munit" % munitVersion % "test") +} diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..8b9a0b0 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.8.0 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..6f0ceba --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,7 @@ +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.12.0") +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.3.4") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.3") +addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.5.0") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.10.4") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.4.18") +addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % "0.4.18")