diff --git a/build.sbt b/build.sbt index b2bdb10..50487b2 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ publishToIW lazy val core = crossProject(JSPlatform, JVMPlatform) - .crossType(CrossType.Pure) + .crossType(CrossType.Full) .settings(name := "iw-support-core") .in(file("core")) .settings(IWDeps.zioPrelude) diff --git a/build.sbt b/build.sbt index b2bdb10..50487b2 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ publishToIW lazy val core = crossProject(JSPlatform, JVMPlatform) - .crossType(CrossType.Pure) + .crossType(CrossType.Full) .settings(name := "iw-support-core") .in(file("core")) .settings(IWDeps.zioPrelude) diff --git a/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala new file mode 100644 index 0000000..640fb29 --- /dev/null +++ b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala @@ -0,0 +1,8 @@ +package works.iterative.core + +import scala.scalajs.js + +trait CzechSupportPlatformSpecific: + given czechOrdering: Ordering[String] with + def compare(x: String, y: String): Int = + x.asInstanceOf[js.Dynamic].localeCompare(y, "cs-CZ").asInstanceOf[Int] diff --git a/build.sbt b/build.sbt index b2bdb10..50487b2 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ publishToIW lazy val core = crossProject(JSPlatform, JVMPlatform) - .crossType(CrossType.Pure) + .crossType(CrossType.Full) .settings(name := "iw-support-core") .in(file("core")) .settings(IWDeps.zioPrelude) diff --git a/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala new file mode 100644 index 0000000..640fb29 --- /dev/null +++ b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala @@ -0,0 +1,8 @@ +package works.iterative.core + +import scala.scalajs.js + +trait CzechSupportPlatformSpecific: + given czechOrdering: Ordering[String] with + def compare(x: String, y: String): Int = + x.asInstanceOf[js.Dynamic].localeCompare(y, "cs-CZ").asInstanceOf[Int] diff --git a/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala b/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala new file mode 100644 index 0000000..9f15ea8 --- /dev/null +++ b/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala @@ -0,0 +1,9 @@ +package works.iterative.core + +trait CzechSupportPlatformSpecific: + given czechOrdering: Ordering[String] = + Ordering.comparatorToOrdering( + java.text.Collator + .getInstance(java.util.Locale.forLanguageTag("cs-CZ")) + .asInstanceOf[java.util.Comparator[String]] + ) diff --git a/build.sbt b/build.sbt index b2bdb10..50487b2 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ publishToIW lazy val core = crossProject(JSPlatform, JVMPlatform) - .crossType(CrossType.Pure) + .crossType(CrossType.Full) .settings(name := "iw-support-core") .in(file("core")) .settings(IWDeps.zioPrelude) diff --git a/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala new file mode 100644 index 0000000..640fb29 --- /dev/null +++ b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala @@ -0,0 +1,8 @@ +package works.iterative.core + +import scala.scalajs.js + +trait CzechSupportPlatformSpecific: + given czechOrdering: Ordering[String] with + def compare(x: String, y: String): Int = + x.asInstanceOf[js.Dynamic].localeCompare(y, "cs-CZ").asInstanceOf[Int] diff --git a/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala b/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala new file mode 100644 index 0000000..9f15ea8 --- /dev/null +++ b/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala @@ -0,0 +1,9 @@ +package works.iterative.core + +trait CzechSupportPlatformSpecific: + given czechOrdering: Ordering[String] = + Ordering.comparatorToOrdering( + java.text.Collator + .getInstance(java.util.Locale.forLanguageTag("cs-CZ")) + .asInstanceOf[java.util.Comparator[String]] + ) diff --git a/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala b/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala new file mode 100644 index 0000000..c174327 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala @@ -0,0 +1,6 @@ +package works.iterative.core + +trait CzechSupport: + given czechOrdering: Ordering[String] + +object CzechSupport extends CzechSupport with CzechSupportPlatformSpecific diff --git a/build.sbt b/build.sbt index b2bdb10..50487b2 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ publishToIW lazy val core = crossProject(JSPlatform, JVMPlatform) - .crossType(CrossType.Pure) + .crossType(CrossType.Full) .settings(name := "iw-support-core") .in(file("core")) .settings(IWDeps.zioPrelude) diff --git a/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala new file mode 100644 index 0000000..640fb29 --- /dev/null +++ b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala @@ -0,0 +1,8 @@ +package works.iterative.core + +import scala.scalajs.js + +trait CzechSupportPlatformSpecific: + given czechOrdering: Ordering[String] with + def compare(x: String, y: String): Int = + x.asInstanceOf[js.Dynamic].localeCompare(y, "cs-CZ").asInstanceOf[Int] diff --git a/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala b/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala new file mode 100644 index 0000000..9f15ea8 --- /dev/null +++ b/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala @@ -0,0 +1,9 @@ +package works.iterative.core + +trait CzechSupportPlatformSpecific: + given czechOrdering: Ordering[String] = + Ordering.comparatorToOrdering( + java.text.Collator + .getInstance(java.util.Locale.forLanguageTag("cs-CZ")) + .asInstanceOf[java.util.Comparator[String]] + ) diff --git a/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala b/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala new file mode 100644 index 0000000..c174327 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala @@ -0,0 +1,6 @@ +package works.iterative.core + +trait CzechSupport: + given czechOrdering: Ordering[String] + +object CzechSupport extends CzechSupport with CzechSupportPlatformSpecific diff --git a/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala b/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala new file mode 100644 index 0000000..ac38528 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala @@ -0,0 +1,14 @@ +package works.iterative +package core + +// TODO: generic message catalogue +// we need to be able to render HTML messages +// like a list of items for example +trait MessageCatalogue: + def apply(id: MessageId): String = + get(id).getOrElse(id.toString()) + def apply(msg: UserMessage): String = + get(msg).getOrElse(msg.id.toString()) + + def get(id: MessageId): Option[String] + def get(msg: UserMessage): Option[String] diff --git a/build.sbt b/build.sbt index b2bdb10..50487b2 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ publishToIW lazy val core = crossProject(JSPlatform, JVMPlatform) - .crossType(CrossType.Pure) + .crossType(CrossType.Full) .settings(name := "iw-support-core") .in(file("core")) .settings(IWDeps.zioPrelude) diff --git a/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala new file mode 100644 index 0000000..640fb29 --- /dev/null +++ b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala @@ -0,0 +1,8 @@ +package works.iterative.core + +import scala.scalajs.js + +trait CzechSupportPlatformSpecific: + given czechOrdering: Ordering[String] with + def compare(x: String, y: String): Int = + x.asInstanceOf[js.Dynamic].localeCompare(y, "cs-CZ").asInstanceOf[Int] diff --git a/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala b/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala new file mode 100644 index 0000000..9f15ea8 --- /dev/null +++ b/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala @@ -0,0 +1,9 @@ +package works.iterative.core + +trait CzechSupportPlatformSpecific: + given czechOrdering: Ordering[String] = + Ordering.comparatorToOrdering( + java.text.Collator + .getInstance(java.util.Locale.forLanguageTag("cs-CZ")) + .asInstanceOf[java.util.Comparator[String]] + ) diff --git a/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala b/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala new file mode 100644 index 0000000..c174327 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala @@ -0,0 +1,6 @@ +package works.iterative.core + +trait CzechSupport: + given czechOrdering: Ordering[String] + +object CzechSupport extends CzechSupport with CzechSupportPlatformSpecific diff --git a/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala b/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala new file mode 100644 index 0000000..ac38528 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala @@ -0,0 +1,14 @@ +package works.iterative +package core + +// TODO: generic message catalogue +// we need to be able to render HTML messages +// like a list of items for example +trait MessageCatalogue: + def apply(id: MessageId): String = + get(id).getOrElse(id.toString()) + def apply(msg: UserMessage): String = + get(msg).getOrElse(msg.id.toString()) + + def get(id: MessageId): Option[String] + def get(msg: UserMessage): Option[String] diff --git a/core/shared/src/main/scala/works/iterative/core/MessageId.scala b/core/shared/src/main/scala/works/iterative/core/MessageId.scala new file mode 100644 index 0000000..72c4777 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/MessageId.scala @@ -0,0 +1,18 @@ +package works.iterative +package core + +// TODO: use refined's NonEmptyString after macros are ported to Scala 3 +// https://github.com/fthomas/refined/issues/932 +// We could than use the inlined macro to validate non empty strings at runtime + +/* MessageId is an opaque type to mark all keys to translations. + * The intent is to use it during build to generate a list of all keys + * and lately to check if we have all the translations we need + */ +opaque type MessageId = String + +object MessageId: + def apply(id: String): MessageId = id + + inline given Conversion[String, MessageId] with + inline def apply(id: String): MessageId = MessageId(id) diff --git a/build.sbt b/build.sbt index b2bdb10..50487b2 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ publishToIW lazy val core = crossProject(JSPlatform, JVMPlatform) - .crossType(CrossType.Pure) + .crossType(CrossType.Full) .settings(name := "iw-support-core") .in(file("core")) .settings(IWDeps.zioPrelude) diff --git a/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala new file mode 100644 index 0000000..640fb29 --- /dev/null +++ b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala @@ -0,0 +1,8 @@ +package works.iterative.core + +import scala.scalajs.js + +trait CzechSupportPlatformSpecific: + given czechOrdering: Ordering[String] with + def compare(x: String, y: String): Int = + x.asInstanceOf[js.Dynamic].localeCompare(y, "cs-CZ").asInstanceOf[Int] diff --git a/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala b/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala new file mode 100644 index 0000000..9f15ea8 --- /dev/null +++ b/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala @@ -0,0 +1,9 @@ +package works.iterative.core + +trait CzechSupportPlatformSpecific: + given czechOrdering: Ordering[String] = + Ordering.comparatorToOrdering( + java.text.Collator + .getInstance(java.util.Locale.forLanguageTag("cs-CZ")) + .asInstanceOf[java.util.Comparator[String]] + ) diff --git a/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala b/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala new file mode 100644 index 0000000..c174327 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala @@ -0,0 +1,6 @@ +package works.iterative.core + +trait CzechSupport: + given czechOrdering: Ordering[String] + +object CzechSupport extends CzechSupport with CzechSupportPlatformSpecific diff --git a/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala b/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala new file mode 100644 index 0000000..ac38528 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala @@ -0,0 +1,14 @@ +package works.iterative +package core + +// TODO: generic message catalogue +// we need to be able to render HTML messages +// like a list of items for example +trait MessageCatalogue: + def apply(id: MessageId): String = + get(id).getOrElse(id.toString()) + def apply(msg: UserMessage): String = + get(msg).getOrElse(msg.id.toString()) + + def get(id: MessageId): Option[String] + def get(msg: UserMessage): Option[String] diff --git a/core/shared/src/main/scala/works/iterative/core/MessageId.scala b/core/shared/src/main/scala/works/iterative/core/MessageId.scala new file mode 100644 index 0000000..72c4777 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/MessageId.scala @@ -0,0 +1,18 @@ +package works.iterative +package core + +// TODO: use refined's NonEmptyString after macros are ported to Scala 3 +// https://github.com/fthomas/refined/issues/932 +// We could than use the inlined macro to validate non empty strings at runtime + +/* MessageId is an opaque type to mark all keys to translations. + * The intent is to use it during build to generate a list of all keys + * and lately to check if we have all the translations we need + */ +opaque type MessageId = String + +object MessageId: + def apply(id: String): MessageId = id + + inline given Conversion[String, MessageId] with + inline def apply(id: String): MessageId = MessageId(id) diff --git a/core/shared/src/main/scala/works/iterative/core/Text.scala b/core/shared/src/main/scala/works/iterative/core/Text.scala new file mode 100644 index 0000000..cccaf59 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/Text.scala @@ -0,0 +1,108 @@ +package works.iterative +package core + +import scala.jdk.OptionConverters.given +import zio.prelude.Validation + +/* UserText represents different, more specific variants of String to be able to + * better identify what kind of String are we looking It is meant to be used + * primarily for UI, but sometimes it is useful to store the different text + * representations variantly, eg. it is useful to have the distinction even on + * the level of the data model, not just in the UI + */ +object Text: + + def nonEmpty(s: String): Option[String] = Option(s).filterNot(_.trim.isEmpty) + + def validateNonEmpty[T](text: T)(using + ev: T =:= String + ): Validation[MessageId, T] = + Validation.fromPredicateWith[MessageId, T]( + "validation.text.empty" + )(text)(t => ev(t).nonEmpty) + + def firstNewLine(t: String): Int = + val lf = t.indexOf('\n') + def cr = t.indexOf('\r') + if lf != -1 then lf else cr + + def hasNewLine(t: String): Boolean = firstNewLine(t) != -1 + + def firstLineOf(t: String): String = + firstNewLine(t) match + case -1 => t + case i => t.take(i) + +opaque type PlainMultiLine = String + +object PlainMultiLine: + def apply(text: String): Validation[MessageId, PlainMultiLine] = + Text.validateNonEmpty(text) + + def opt(text: String): Validation[Nothing, Option[PlainMultiLine]] = + Validation.succeed(optDirect(text)) + + def optDirect(text: String): Option[PlainMultiLine] = + Text.nonEmpty(text) + + given string2plainMultiline: Conversion[String, Option[PlainMultiLine]] with + def apply(text: String): Option[PlainMultiLine] = optDirect(text) + + given optString2PlainMultiline + : Conversion[Option[String], Option[PlainMultiLine]] with + def apply(text: Option[String]): Option[PlainMultiLine] = + text.flatMap(optDirect) + + given plainMultiLine2String: Conversion[PlainMultiLine, String] with + def apply(p: PlainMultiLine): String = p.toString + + given optionPlainMultiLine2OptionString + : Conversion[Option[PlainMultiLine], Option[String]] with + def apply(p: Option[PlainMultiLine]): Option[String] = p.map(_.toString) + + extension (p: PlainMultiLine) def toString: String = p + +opaque type PlainOneLine = String + +object PlainOneLine: + def validateOneLine(text: PlainOneLine): Validation[MessageId, PlainOneLine] = + Validation.fromPredicateWith[MessageId, PlainOneLine]( + "validation.text.oneline" + )(text)(!Text.hasNewLine(_)) + + def apply(text: String): Validation[MessageId, PlainOneLine] = + for + _ <- Text.validateNonEmpty(text) + _ <- validateOneLine(text) + yield text + + def opt(text: String): Validation[MessageId, Option[PlainOneLine]] = + for _ <- validateOneLine(text) + yield Text.nonEmpty(text) + + def optFirstLine(text: String): Option[PlainOneLine] = + Text.nonEmpty(Text.firstLineOf(text)) + + def firstLine(text: String, default: => String): PlainOneLine = + optFirstLine(text).getOrElse(default) + + def firstLineEmpty(text: String): PlainOneLine = firstLine(text, "") + + given string2FirstLineEmpty: Conversion[String, PlainOneLine] = + firstLineEmpty(_) + + extension (p: PlainOneLine) def toString: String = p + +opaque type Markdown = String + +object Markdown: + def apply(text: String): Validation[MessageId, Markdown] = + Text.validateNonEmpty(text) + + def opt(text: String): Validation[Nothing, Option[Markdown]] = + Validation.succeed(optDirect(text)) + + def optDirect(text: String): Option[Markdown] = + Text.nonEmpty(text) + + extension (p: Markdown) def toString: String = p diff --git a/build.sbt b/build.sbt index b2bdb10..50487b2 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ publishToIW lazy val core = crossProject(JSPlatform, JVMPlatform) - .crossType(CrossType.Pure) + .crossType(CrossType.Full) .settings(name := "iw-support-core") .in(file("core")) .settings(IWDeps.zioPrelude) diff --git a/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala new file mode 100644 index 0000000..640fb29 --- /dev/null +++ b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala @@ -0,0 +1,8 @@ +package works.iterative.core + +import scala.scalajs.js + +trait CzechSupportPlatformSpecific: + given czechOrdering: Ordering[String] with + def compare(x: String, y: String): Int = + x.asInstanceOf[js.Dynamic].localeCompare(y, "cs-CZ").asInstanceOf[Int] diff --git a/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala b/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala new file mode 100644 index 0000000..9f15ea8 --- /dev/null +++ b/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala @@ -0,0 +1,9 @@ +package works.iterative.core + +trait CzechSupportPlatformSpecific: + given czechOrdering: Ordering[String] = + Ordering.comparatorToOrdering( + java.text.Collator + .getInstance(java.util.Locale.forLanguageTag("cs-CZ")) + .asInstanceOf[java.util.Comparator[String]] + ) diff --git a/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala b/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala new file mode 100644 index 0000000..c174327 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala @@ -0,0 +1,6 @@ +package works.iterative.core + +trait CzechSupport: + given czechOrdering: Ordering[String] + +object CzechSupport extends CzechSupport with CzechSupportPlatformSpecific diff --git a/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala b/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala new file mode 100644 index 0000000..ac38528 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala @@ -0,0 +1,14 @@ +package works.iterative +package core + +// TODO: generic message catalogue +// we need to be able to render HTML messages +// like a list of items for example +trait MessageCatalogue: + def apply(id: MessageId): String = + get(id).getOrElse(id.toString()) + def apply(msg: UserMessage): String = + get(msg).getOrElse(msg.id.toString()) + + def get(id: MessageId): Option[String] + def get(msg: UserMessage): Option[String] diff --git a/core/shared/src/main/scala/works/iterative/core/MessageId.scala b/core/shared/src/main/scala/works/iterative/core/MessageId.scala new file mode 100644 index 0000000..72c4777 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/MessageId.scala @@ -0,0 +1,18 @@ +package works.iterative +package core + +// TODO: use refined's NonEmptyString after macros are ported to Scala 3 +// https://github.com/fthomas/refined/issues/932 +// We could than use the inlined macro to validate non empty strings at runtime + +/* MessageId is an opaque type to mark all keys to translations. + * The intent is to use it during build to generate a list of all keys + * and lately to check if we have all the translations we need + */ +opaque type MessageId = String + +object MessageId: + def apply(id: String): MessageId = id + + inline given Conversion[String, MessageId] with + inline def apply(id: String): MessageId = MessageId(id) diff --git a/core/shared/src/main/scala/works/iterative/core/Text.scala b/core/shared/src/main/scala/works/iterative/core/Text.scala new file mode 100644 index 0000000..cccaf59 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/Text.scala @@ -0,0 +1,108 @@ +package works.iterative +package core + +import scala.jdk.OptionConverters.given +import zio.prelude.Validation + +/* UserText represents different, more specific variants of String to be able to + * better identify what kind of String are we looking It is meant to be used + * primarily for UI, but sometimes it is useful to store the different text + * representations variantly, eg. it is useful to have the distinction even on + * the level of the data model, not just in the UI + */ +object Text: + + def nonEmpty(s: String): Option[String] = Option(s).filterNot(_.trim.isEmpty) + + def validateNonEmpty[T](text: T)(using + ev: T =:= String + ): Validation[MessageId, T] = + Validation.fromPredicateWith[MessageId, T]( + "validation.text.empty" + )(text)(t => ev(t).nonEmpty) + + def firstNewLine(t: String): Int = + val lf = t.indexOf('\n') + def cr = t.indexOf('\r') + if lf != -1 then lf else cr + + def hasNewLine(t: String): Boolean = firstNewLine(t) != -1 + + def firstLineOf(t: String): String = + firstNewLine(t) match + case -1 => t + case i => t.take(i) + +opaque type PlainMultiLine = String + +object PlainMultiLine: + def apply(text: String): Validation[MessageId, PlainMultiLine] = + Text.validateNonEmpty(text) + + def opt(text: String): Validation[Nothing, Option[PlainMultiLine]] = + Validation.succeed(optDirect(text)) + + def optDirect(text: String): Option[PlainMultiLine] = + Text.nonEmpty(text) + + given string2plainMultiline: Conversion[String, Option[PlainMultiLine]] with + def apply(text: String): Option[PlainMultiLine] = optDirect(text) + + given optString2PlainMultiline + : Conversion[Option[String], Option[PlainMultiLine]] with + def apply(text: Option[String]): Option[PlainMultiLine] = + text.flatMap(optDirect) + + given plainMultiLine2String: Conversion[PlainMultiLine, String] with + def apply(p: PlainMultiLine): String = p.toString + + given optionPlainMultiLine2OptionString + : Conversion[Option[PlainMultiLine], Option[String]] with + def apply(p: Option[PlainMultiLine]): Option[String] = p.map(_.toString) + + extension (p: PlainMultiLine) def toString: String = p + +opaque type PlainOneLine = String + +object PlainOneLine: + def validateOneLine(text: PlainOneLine): Validation[MessageId, PlainOneLine] = + Validation.fromPredicateWith[MessageId, PlainOneLine]( + "validation.text.oneline" + )(text)(!Text.hasNewLine(_)) + + def apply(text: String): Validation[MessageId, PlainOneLine] = + for + _ <- Text.validateNonEmpty(text) + _ <- validateOneLine(text) + yield text + + def opt(text: String): Validation[MessageId, Option[PlainOneLine]] = + for _ <- validateOneLine(text) + yield Text.nonEmpty(text) + + def optFirstLine(text: String): Option[PlainOneLine] = + Text.nonEmpty(Text.firstLineOf(text)) + + def firstLine(text: String, default: => String): PlainOneLine = + optFirstLine(text).getOrElse(default) + + def firstLineEmpty(text: String): PlainOneLine = firstLine(text, "") + + given string2FirstLineEmpty: Conversion[String, PlainOneLine] = + firstLineEmpty(_) + + extension (p: PlainOneLine) def toString: String = p + +opaque type Markdown = String + +object Markdown: + def apply(text: String): Validation[MessageId, Markdown] = + Text.validateNonEmpty(text) + + def opt(text: String): Validation[Nothing, Option[Markdown]] = + Validation.succeed(optDirect(text)) + + def optDirect(text: String): Option[Markdown] = + Text.nonEmpty(text) + + extension (p: Markdown) def toString: String = p diff --git a/core/shared/src/main/scala/works/iterative/core/UserMessage.scala b/core/shared/src/main/scala/works/iterative/core/UserMessage.scala new file mode 100644 index 0000000..d2d9343 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/UserMessage.scala @@ -0,0 +1,6 @@ +package works.iterative +package core + +// Type-wise naive solution to speicifying user messages. +// A mechanism that will check the message for correct formatting and validate parameters is needed +case class UserMessage(id: MessageId, args: Any*) diff --git a/build.sbt b/build.sbt index b2bdb10..50487b2 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ publishToIW lazy val core = crossProject(JSPlatform, JVMPlatform) - .crossType(CrossType.Pure) + .crossType(CrossType.Full) .settings(name := "iw-support-core") .in(file("core")) .settings(IWDeps.zioPrelude) diff --git a/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala new file mode 100644 index 0000000..640fb29 --- /dev/null +++ b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala @@ -0,0 +1,8 @@ +package works.iterative.core + +import scala.scalajs.js + +trait CzechSupportPlatformSpecific: + given czechOrdering: Ordering[String] with + def compare(x: String, y: String): Int = + x.asInstanceOf[js.Dynamic].localeCompare(y, "cs-CZ").asInstanceOf[Int] diff --git a/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala b/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala new file mode 100644 index 0000000..9f15ea8 --- /dev/null +++ b/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala @@ -0,0 +1,9 @@ +package works.iterative.core + +trait CzechSupportPlatformSpecific: + given czechOrdering: Ordering[String] = + Ordering.comparatorToOrdering( + java.text.Collator + .getInstance(java.util.Locale.forLanguageTag("cs-CZ")) + .asInstanceOf[java.util.Comparator[String]] + ) diff --git a/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala b/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala new file mode 100644 index 0000000..c174327 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala @@ -0,0 +1,6 @@ +package works.iterative.core + +trait CzechSupport: + given czechOrdering: Ordering[String] + +object CzechSupport extends CzechSupport with CzechSupportPlatformSpecific diff --git a/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala b/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala new file mode 100644 index 0000000..ac38528 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala @@ -0,0 +1,14 @@ +package works.iterative +package core + +// TODO: generic message catalogue +// we need to be able to render HTML messages +// like a list of items for example +trait MessageCatalogue: + def apply(id: MessageId): String = + get(id).getOrElse(id.toString()) + def apply(msg: UserMessage): String = + get(msg).getOrElse(msg.id.toString()) + + def get(id: MessageId): Option[String] + def get(msg: UserMessage): Option[String] diff --git a/core/shared/src/main/scala/works/iterative/core/MessageId.scala b/core/shared/src/main/scala/works/iterative/core/MessageId.scala new file mode 100644 index 0000000..72c4777 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/MessageId.scala @@ -0,0 +1,18 @@ +package works.iterative +package core + +// TODO: use refined's NonEmptyString after macros are ported to Scala 3 +// https://github.com/fthomas/refined/issues/932 +// We could than use the inlined macro to validate non empty strings at runtime + +/* MessageId is an opaque type to mark all keys to translations. + * The intent is to use it during build to generate a list of all keys + * and lately to check if we have all the translations we need + */ +opaque type MessageId = String + +object MessageId: + def apply(id: String): MessageId = id + + inline given Conversion[String, MessageId] with + inline def apply(id: String): MessageId = MessageId(id) diff --git a/core/shared/src/main/scala/works/iterative/core/Text.scala b/core/shared/src/main/scala/works/iterative/core/Text.scala new file mode 100644 index 0000000..cccaf59 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/Text.scala @@ -0,0 +1,108 @@ +package works.iterative +package core + +import scala.jdk.OptionConverters.given +import zio.prelude.Validation + +/* UserText represents different, more specific variants of String to be able to + * better identify what kind of String are we looking It is meant to be used + * primarily for UI, but sometimes it is useful to store the different text + * representations variantly, eg. it is useful to have the distinction even on + * the level of the data model, not just in the UI + */ +object Text: + + def nonEmpty(s: String): Option[String] = Option(s).filterNot(_.trim.isEmpty) + + def validateNonEmpty[T](text: T)(using + ev: T =:= String + ): Validation[MessageId, T] = + Validation.fromPredicateWith[MessageId, T]( + "validation.text.empty" + )(text)(t => ev(t).nonEmpty) + + def firstNewLine(t: String): Int = + val lf = t.indexOf('\n') + def cr = t.indexOf('\r') + if lf != -1 then lf else cr + + def hasNewLine(t: String): Boolean = firstNewLine(t) != -1 + + def firstLineOf(t: String): String = + firstNewLine(t) match + case -1 => t + case i => t.take(i) + +opaque type PlainMultiLine = String + +object PlainMultiLine: + def apply(text: String): Validation[MessageId, PlainMultiLine] = + Text.validateNonEmpty(text) + + def opt(text: String): Validation[Nothing, Option[PlainMultiLine]] = + Validation.succeed(optDirect(text)) + + def optDirect(text: String): Option[PlainMultiLine] = + Text.nonEmpty(text) + + given string2plainMultiline: Conversion[String, Option[PlainMultiLine]] with + def apply(text: String): Option[PlainMultiLine] = optDirect(text) + + given optString2PlainMultiline + : Conversion[Option[String], Option[PlainMultiLine]] with + def apply(text: Option[String]): Option[PlainMultiLine] = + text.flatMap(optDirect) + + given plainMultiLine2String: Conversion[PlainMultiLine, String] with + def apply(p: PlainMultiLine): String = p.toString + + given optionPlainMultiLine2OptionString + : Conversion[Option[PlainMultiLine], Option[String]] with + def apply(p: Option[PlainMultiLine]): Option[String] = p.map(_.toString) + + extension (p: PlainMultiLine) def toString: String = p + +opaque type PlainOneLine = String + +object PlainOneLine: + def validateOneLine(text: PlainOneLine): Validation[MessageId, PlainOneLine] = + Validation.fromPredicateWith[MessageId, PlainOneLine]( + "validation.text.oneline" + )(text)(!Text.hasNewLine(_)) + + def apply(text: String): Validation[MessageId, PlainOneLine] = + for + _ <- Text.validateNonEmpty(text) + _ <- validateOneLine(text) + yield text + + def opt(text: String): Validation[MessageId, Option[PlainOneLine]] = + for _ <- validateOneLine(text) + yield Text.nonEmpty(text) + + def optFirstLine(text: String): Option[PlainOneLine] = + Text.nonEmpty(Text.firstLineOf(text)) + + def firstLine(text: String, default: => String): PlainOneLine = + optFirstLine(text).getOrElse(default) + + def firstLineEmpty(text: String): PlainOneLine = firstLine(text, "") + + given string2FirstLineEmpty: Conversion[String, PlainOneLine] = + firstLineEmpty(_) + + extension (p: PlainOneLine) def toString: String = p + +opaque type Markdown = String + +object Markdown: + def apply(text: String): Validation[MessageId, Markdown] = + Text.validateNonEmpty(text) + + def opt(text: String): Validation[Nothing, Option[Markdown]] = + Validation.succeed(optDirect(text)) + + def optDirect(text: String): Option[Markdown] = + Text.nonEmpty(text) + + extension (p: Markdown) def toString: String = p diff --git a/core/shared/src/main/scala/works/iterative/core/UserMessage.scala b/core/shared/src/main/scala/works/iterative/core/UserMessage.scala new file mode 100644 index 0000000..d2d9343 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/UserMessage.scala @@ -0,0 +1,6 @@ +package works.iterative +package core + +// Type-wise naive solution to speicifying user messages. +// A mechanism that will check the message for correct formatting and validate parameters is needed +case class UserMessage(id: MessageId, args: Any*) diff --git a/core/src/main/scala/works/iterative/core/MessageCatalogue.scala b/core/src/main/scala/works/iterative/core/MessageCatalogue.scala deleted file mode 100644 index ac38528..0000000 --- a/core/src/main/scala/works/iterative/core/MessageCatalogue.scala +++ /dev/null @@ -1,14 +0,0 @@ -package works.iterative -package core - -// TODO: generic message catalogue -// we need to be able to render HTML messages -// like a list of items for example -trait MessageCatalogue: - def apply(id: MessageId): String = - get(id).getOrElse(id.toString()) - def apply(msg: UserMessage): String = - get(msg).getOrElse(msg.id.toString()) - - def get(id: MessageId): Option[String] - def get(msg: UserMessage): Option[String] diff --git a/build.sbt b/build.sbt index b2bdb10..50487b2 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ publishToIW lazy val core = crossProject(JSPlatform, JVMPlatform) - .crossType(CrossType.Pure) + .crossType(CrossType.Full) .settings(name := "iw-support-core") .in(file("core")) .settings(IWDeps.zioPrelude) diff --git a/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala new file mode 100644 index 0000000..640fb29 --- /dev/null +++ b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala @@ -0,0 +1,8 @@ +package works.iterative.core + +import scala.scalajs.js + +trait CzechSupportPlatformSpecific: + given czechOrdering: Ordering[String] with + def compare(x: String, y: String): Int = + x.asInstanceOf[js.Dynamic].localeCompare(y, "cs-CZ").asInstanceOf[Int] diff --git a/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala b/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala new file mode 100644 index 0000000..9f15ea8 --- /dev/null +++ b/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala @@ -0,0 +1,9 @@ +package works.iterative.core + +trait CzechSupportPlatformSpecific: + given czechOrdering: Ordering[String] = + Ordering.comparatorToOrdering( + java.text.Collator + .getInstance(java.util.Locale.forLanguageTag("cs-CZ")) + .asInstanceOf[java.util.Comparator[String]] + ) diff --git a/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala b/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala new file mode 100644 index 0000000..c174327 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala @@ -0,0 +1,6 @@ +package works.iterative.core + +trait CzechSupport: + given czechOrdering: Ordering[String] + +object CzechSupport extends CzechSupport with CzechSupportPlatformSpecific diff --git a/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala b/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala new file mode 100644 index 0000000..ac38528 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala @@ -0,0 +1,14 @@ +package works.iterative +package core + +// TODO: generic message catalogue +// we need to be able to render HTML messages +// like a list of items for example +trait MessageCatalogue: + def apply(id: MessageId): String = + get(id).getOrElse(id.toString()) + def apply(msg: UserMessage): String = + get(msg).getOrElse(msg.id.toString()) + + def get(id: MessageId): Option[String] + def get(msg: UserMessage): Option[String] diff --git a/core/shared/src/main/scala/works/iterative/core/MessageId.scala b/core/shared/src/main/scala/works/iterative/core/MessageId.scala new file mode 100644 index 0000000..72c4777 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/MessageId.scala @@ -0,0 +1,18 @@ +package works.iterative +package core + +// TODO: use refined's NonEmptyString after macros are ported to Scala 3 +// https://github.com/fthomas/refined/issues/932 +// We could than use the inlined macro to validate non empty strings at runtime + +/* MessageId is an opaque type to mark all keys to translations. + * The intent is to use it during build to generate a list of all keys + * and lately to check if we have all the translations we need + */ +opaque type MessageId = String + +object MessageId: + def apply(id: String): MessageId = id + + inline given Conversion[String, MessageId] with + inline def apply(id: String): MessageId = MessageId(id) diff --git a/core/shared/src/main/scala/works/iterative/core/Text.scala b/core/shared/src/main/scala/works/iterative/core/Text.scala new file mode 100644 index 0000000..cccaf59 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/Text.scala @@ -0,0 +1,108 @@ +package works.iterative +package core + +import scala.jdk.OptionConverters.given +import zio.prelude.Validation + +/* UserText represents different, more specific variants of String to be able to + * better identify what kind of String are we looking It is meant to be used + * primarily for UI, but sometimes it is useful to store the different text + * representations variantly, eg. it is useful to have the distinction even on + * the level of the data model, not just in the UI + */ +object Text: + + def nonEmpty(s: String): Option[String] = Option(s).filterNot(_.trim.isEmpty) + + def validateNonEmpty[T](text: T)(using + ev: T =:= String + ): Validation[MessageId, T] = + Validation.fromPredicateWith[MessageId, T]( + "validation.text.empty" + )(text)(t => ev(t).nonEmpty) + + def firstNewLine(t: String): Int = + val lf = t.indexOf('\n') + def cr = t.indexOf('\r') + if lf != -1 then lf else cr + + def hasNewLine(t: String): Boolean = firstNewLine(t) != -1 + + def firstLineOf(t: String): String = + firstNewLine(t) match + case -1 => t + case i => t.take(i) + +opaque type PlainMultiLine = String + +object PlainMultiLine: + def apply(text: String): Validation[MessageId, PlainMultiLine] = + Text.validateNonEmpty(text) + + def opt(text: String): Validation[Nothing, Option[PlainMultiLine]] = + Validation.succeed(optDirect(text)) + + def optDirect(text: String): Option[PlainMultiLine] = + Text.nonEmpty(text) + + given string2plainMultiline: Conversion[String, Option[PlainMultiLine]] with + def apply(text: String): Option[PlainMultiLine] = optDirect(text) + + given optString2PlainMultiline + : Conversion[Option[String], Option[PlainMultiLine]] with + def apply(text: Option[String]): Option[PlainMultiLine] = + text.flatMap(optDirect) + + given plainMultiLine2String: Conversion[PlainMultiLine, String] with + def apply(p: PlainMultiLine): String = p.toString + + given optionPlainMultiLine2OptionString + : Conversion[Option[PlainMultiLine], Option[String]] with + def apply(p: Option[PlainMultiLine]): Option[String] = p.map(_.toString) + + extension (p: PlainMultiLine) def toString: String = p + +opaque type PlainOneLine = String + +object PlainOneLine: + def validateOneLine(text: PlainOneLine): Validation[MessageId, PlainOneLine] = + Validation.fromPredicateWith[MessageId, PlainOneLine]( + "validation.text.oneline" + )(text)(!Text.hasNewLine(_)) + + def apply(text: String): Validation[MessageId, PlainOneLine] = + for + _ <- Text.validateNonEmpty(text) + _ <- validateOneLine(text) + yield text + + def opt(text: String): Validation[MessageId, Option[PlainOneLine]] = + for _ <- validateOneLine(text) + yield Text.nonEmpty(text) + + def optFirstLine(text: String): Option[PlainOneLine] = + Text.nonEmpty(Text.firstLineOf(text)) + + def firstLine(text: String, default: => String): PlainOneLine = + optFirstLine(text).getOrElse(default) + + def firstLineEmpty(text: String): PlainOneLine = firstLine(text, "") + + given string2FirstLineEmpty: Conversion[String, PlainOneLine] = + firstLineEmpty(_) + + extension (p: PlainOneLine) def toString: String = p + +opaque type Markdown = String + +object Markdown: + def apply(text: String): Validation[MessageId, Markdown] = + Text.validateNonEmpty(text) + + def opt(text: String): Validation[Nothing, Option[Markdown]] = + Validation.succeed(optDirect(text)) + + def optDirect(text: String): Option[Markdown] = + Text.nonEmpty(text) + + extension (p: Markdown) def toString: String = p diff --git a/core/shared/src/main/scala/works/iterative/core/UserMessage.scala b/core/shared/src/main/scala/works/iterative/core/UserMessage.scala new file mode 100644 index 0000000..d2d9343 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/UserMessage.scala @@ -0,0 +1,6 @@ +package works.iterative +package core + +// Type-wise naive solution to speicifying user messages. +// A mechanism that will check the message for correct formatting and validate parameters is needed +case class UserMessage(id: MessageId, args: Any*) diff --git a/core/src/main/scala/works/iterative/core/MessageCatalogue.scala b/core/src/main/scala/works/iterative/core/MessageCatalogue.scala deleted file mode 100644 index ac38528..0000000 --- a/core/src/main/scala/works/iterative/core/MessageCatalogue.scala +++ /dev/null @@ -1,14 +0,0 @@ -package works.iterative -package core - -// TODO: generic message catalogue -// we need to be able to render HTML messages -// like a list of items for example -trait MessageCatalogue: - def apply(id: MessageId): String = - get(id).getOrElse(id.toString()) - def apply(msg: UserMessage): String = - get(msg).getOrElse(msg.id.toString()) - - def get(id: MessageId): Option[String] - def get(msg: UserMessage): Option[String] diff --git a/core/src/main/scala/works/iterative/core/MessageId.scala b/core/src/main/scala/works/iterative/core/MessageId.scala deleted file mode 100644 index 72c4777..0000000 --- a/core/src/main/scala/works/iterative/core/MessageId.scala +++ /dev/null @@ -1,18 +0,0 @@ -package works.iterative -package core - -// TODO: use refined's NonEmptyString after macros are ported to Scala 3 -// https://github.com/fthomas/refined/issues/932 -// We could than use the inlined macro to validate non empty strings at runtime - -/* MessageId is an opaque type to mark all keys to translations. - * The intent is to use it during build to generate a list of all keys - * and lately to check if we have all the translations we need - */ -opaque type MessageId = String - -object MessageId: - def apply(id: String): MessageId = id - - inline given Conversion[String, MessageId] with - inline def apply(id: String): MessageId = MessageId(id) diff --git a/build.sbt b/build.sbt index b2bdb10..50487b2 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ publishToIW lazy val core = crossProject(JSPlatform, JVMPlatform) - .crossType(CrossType.Pure) + .crossType(CrossType.Full) .settings(name := "iw-support-core") .in(file("core")) .settings(IWDeps.zioPrelude) diff --git a/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala new file mode 100644 index 0000000..640fb29 --- /dev/null +++ b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala @@ -0,0 +1,8 @@ +package works.iterative.core + +import scala.scalajs.js + +trait CzechSupportPlatformSpecific: + given czechOrdering: Ordering[String] with + def compare(x: String, y: String): Int = + x.asInstanceOf[js.Dynamic].localeCompare(y, "cs-CZ").asInstanceOf[Int] diff --git a/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala b/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala new file mode 100644 index 0000000..9f15ea8 --- /dev/null +++ b/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala @@ -0,0 +1,9 @@ +package works.iterative.core + +trait CzechSupportPlatformSpecific: + given czechOrdering: Ordering[String] = + Ordering.comparatorToOrdering( + java.text.Collator + .getInstance(java.util.Locale.forLanguageTag("cs-CZ")) + .asInstanceOf[java.util.Comparator[String]] + ) diff --git a/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala b/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala new file mode 100644 index 0000000..c174327 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala @@ -0,0 +1,6 @@ +package works.iterative.core + +trait CzechSupport: + given czechOrdering: Ordering[String] + +object CzechSupport extends CzechSupport with CzechSupportPlatformSpecific diff --git a/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala b/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala new file mode 100644 index 0000000..ac38528 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala @@ -0,0 +1,14 @@ +package works.iterative +package core + +// TODO: generic message catalogue +// we need to be able to render HTML messages +// like a list of items for example +trait MessageCatalogue: + def apply(id: MessageId): String = + get(id).getOrElse(id.toString()) + def apply(msg: UserMessage): String = + get(msg).getOrElse(msg.id.toString()) + + def get(id: MessageId): Option[String] + def get(msg: UserMessage): Option[String] diff --git a/core/shared/src/main/scala/works/iterative/core/MessageId.scala b/core/shared/src/main/scala/works/iterative/core/MessageId.scala new file mode 100644 index 0000000..72c4777 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/MessageId.scala @@ -0,0 +1,18 @@ +package works.iterative +package core + +// TODO: use refined's NonEmptyString after macros are ported to Scala 3 +// https://github.com/fthomas/refined/issues/932 +// We could than use the inlined macro to validate non empty strings at runtime + +/* MessageId is an opaque type to mark all keys to translations. + * The intent is to use it during build to generate a list of all keys + * and lately to check if we have all the translations we need + */ +opaque type MessageId = String + +object MessageId: + def apply(id: String): MessageId = id + + inline given Conversion[String, MessageId] with + inline def apply(id: String): MessageId = MessageId(id) diff --git a/core/shared/src/main/scala/works/iterative/core/Text.scala b/core/shared/src/main/scala/works/iterative/core/Text.scala new file mode 100644 index 0000000..cccaf59 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/Text.scala @@ -0,0 +1,108 @@ +package works.iterative +package core + +import scala.jdk.OptionConverters.given +import zio.prelude.Validation + +/* UserText represents different, more specific variants of String to be able to + * better identify what kind of String are we looking It is meant to be used + * primarily for UI, but sometimes it is useful to store the different text + * representations variantly, eg. it is useful to have the distinction even on + * the level of the data model, not just in the UI + */ +object Text: + + def nonEmpty(s: String): Option[String] = Option(s).filterNot(_.trim.isEmpty) + + def validateNonEmpty[T](text: T)(using + ev: T =:= String + ): Validation[MessageId, T] = + Validation.fromPredicateWith[MessageId, T]( + "validation.text.empty" + )(text)(t => ev(t).nonEmpty) + + def firstNewLine(t: String): Int = + val lf = t.indexOf('\n') + def cr = t.indexOf('\r') + if lf != -1 then lf else cr + + def hasNewLine(t: String): Boolean = firstNewLine(t) != -1 + + def firstLineOf(t: String): String = + firstNewLine(t) match + case -1 => t + case i => t.take(i) + +opaque type PlainMultiLine = String + +object PlainMultiLine: + def apply(text: String): Validation[MessageId, PlainMultiLine] = + Text.validateNonEmpty(text) + + def opt(text: String): Validation[Nothing, Option[PlainMultiLine]] = + Validation.succeed(optDirect(text)) + + def optDirect(text: String): Option[PlainMultiLine] = + Text.nonEmpty(text) + + given string2plainMultiline: Conversion[String, Option[PlainMultiLine]] with + def apply(text: String): Option[PlainMultiLine] = optDirect(text) + + given optString2PlainMultiline + : Conversion[Option[String], Option[PlainMultiLine]] with + def apply(text: Option[String]): Option[PlainMultiLine] = + text.flatMap(optDirect) + + given plainMultiLine2String: Conversion[PlainMultiLine, String] with + def apply(p: PlainMultiLine): String = p.toString + + given optionPlainMultiLine2OptionString + : Conversion[Option[PlainMultiLine], Option[String]] with + def apply(p: Option[PlainMultiLine]): Option[String] = p.map(_.toString) + + extension (p: PlainMultiLine) def toString: String = p + +opaque type PlainOneLine = String + +object PlainOneLine: + def validateOneLine(text: PlainOneLine): Validation[MessageId, PlainOneLine] = + Validation.fromPredicateWith[MessageId, PlainOneLine]( + "validation.text.oneline" + )(text)(!Text.hasNewLine(_)) + + def apply(text: String): Validation[MessageId, PlainOneLine] = + for + _ <- Text.validateNonEmpty(text) + _ <- validateOneLine(text) + yield text + + def opt(text: String): Validation[MessageId, Option[PlainOneLine]] = + for _ <- validateOneLine(text) + yield Text.nonEmpty(text) + + def optFirstLine(text: String): Option[PlainOneLine] = + Text.nonEmpty(Text.firstLineOf(text)) + + def firstLine(text: String, default: => String): PlainOneLine = + optFirstLine(text).getOrElse(default) + + def firstLineEmpty(text: String): PlainOneLine = firstLine(text, "") + + given string2FirstLineEmpty: Conversion[String, PlainOneLine] = + firstLineEmpty(_) + + extension (p: PlainOneLine) def toString: String = p + +opaque type Markdown = String + +object Markdown: + def apply(text: String): Validation[MessageId, Markdown] = + Text.validateNonEmpty(text) + + def opt(text: String): Validation[Nothing, Option[Markdown]] = + Validation.succeed(optDirect(text)) + + def optDirect(text: String): Option[Markdown] = + Text.nonEmpty(text) + + extension (p: Markdown) def toString: String = p diff --git a/core/shared/src/main/scala/works/iterative/core/UserMessage.scala b/core/shared/src/main/scala/works/iterative/core/UserMessage.scala new file mode 100644 index 0000000..d2d9343 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/UserMessage.scala @@ -0,0 +1,6 @@ +package works.iterative +package core + +// Type-wise naive solution to speicifying user messages. +// A mechanism that will check the message for correct formatting and validate parameters is needed +case class UserMessage(id: MessageId, args: Any*) diff --git a/core/src/main/scala/works/iterative/core/MessageCatalogue.scala b/core/src/main/scala/works/iterative/core/MessageCatalogue.scala deleted file mode 100644 index ac38528..0000000 --- a/core/src/main/scala/works/iterative/core/MessageCatalogue.scala +++ /dev/null @@ -1,14 +0,0 @@ -package works.iterative -package core - -// TODO: generic message catalogue -// we need to be able to render HTML messages -// like a list of items for example -trait MessageCatalogue: - def apply(id: MessageId): String = - get(id).getOrElse(id.toString()) - def apply(msg: UserMessage): String = - get(msg).getOrElse(msg.id.toString()) - - def get(id: MessageId): Option[String] - def get(msg: UserMessage): Option[String] diff --git a/core/src/main/scala/works/iterative/core/MessageId.scala b/core/src/main/scala/works/iterative/core/MessageId.scala deleted file mode 100644 index 72c4777..0000000 --- a/core/src/main/scala/works/iterative/core/MessageId.scala +++ /dev/null @@ -1,18 +0,0 @@ -package works.iterative -package core - -// TODO: use refined's NonEmptyString after macros are ported to Scala 3 -// https://github.com/fthomas/refined/issues/932 -// We could than use the inlined macro to validate non empty strings at runtime - -/* MessageId is an opaque type to mark all keys to translations. - * The intent is to use it during build to generate a list of all keys - * and lately to check if we have all the translations we need - */ -opaque type MessageId = String - -object MessageId: - def apply(id: String): MessageId = id - - inline given Conversion[String, MessageId] with - inline def apply(id: String): MessageId = MessageId(id) diff --git a/core/src/main/scala/works/iterative/core/Text.scala b/core/src/main/scala/works/iterative/core/Text.scala deleted file mode 100644 index cccaf59..0000000 --- a/core/src/main/scala/works/iterative/core/Text.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative -package core - -import scala.jdk.OptionConverters.given -import zio.prelude.Validation - -/* UserText represents different, more specific variants of String to be able to - * better identify what kind of String are we looking It is meant to be used - * primarily for UI, but sometimes it is useful to store the different text - * representations variantly, eg. it is useful to have the distinction even on - * the level of the data model, not just in the UI - */ -object Text: - - def nonEmpty(s: String): Option[String] = Option(s).filterNot(_.trim.isEmpty) - - def validateNonEmpty[T](text: T)(using - ev: T =:= String - ): Validation[MessageId, T] = - Validation.fromPredicateWith[MessageId, T]( - "validation.text.empty" - )(text)(t => ev(t).nonEmpty) - - def firstNewLine(t: String): Int = - val lf = t.indexOf('\n') - def cr = t.indexOf('\r') - if lf != -1 then lf else cr - - def hasNewLine(t: String): Boolean = firstNewLine(t) != -1 - - def firstLineOf(t: String): String = - firstNewLine(t) match - case -1 => t - case i => t.take(i) - -opaque type PlainMultiLine = String - -object PlainMultiLine: - def apply(text: String): Validation[MessageId, PlainMultiLine] = - Text.validateNonEmpty(text) - - def opt(text: String): Validation[Nothing, Option[PlainMultiLine]] = - Validation.succeed(optDirect(text)) - - def optDirect(text: String): Option[PlainMultiLine] = - Text.nonEmpty(text) - - given string2plainMultiline: Conversion[String, Option[PlainMultiLine]] with - def apply(text: String): Option[PlainMultiLine] = optDirect(text) - - given optString2PlainMultiline - : Conversion[Option[String], Option[PlainMultiLine]] with - def apply(text: Option[String]): Option[PlainMultiLine] = - text.flatMap(optDirect) - - given plainMultiLine2String: Conversion[PlainMultiLine, String] with - def apply(p: PlainMultiLine): String = p.toString - - given optionPlainMultiLine2OptionString - : Conversion[Option[PlainMultiLine], Option[String]] with - def apply(p: Option[PlainMultiLine]): Option[String] = p.map(_.toString) - - extension (p: PlainMultiLine) def toString: String = p - -opaque type PlainOneLine = String - -object PlainOneLine: - def validateOneLine(text: PlainOneLine): Validation[MessageId, PlainOneLine] = - Validation.fromPredicateWith[MessageId, PlainOneLine]( - "validation.text.oneline" - )(text)(!Text.hasNewLine(_)) - - def apply(text: String): Validation[MessageId, PlainOneLine] = - for - _ <- Text.validateNonEmpty(text) - _ <- validateOneLine(text) - yield text - - def opt(text: String): Validation[MessageId, Option[PlainOneLine]] = - for _ <- validateOneLine(text) - yield Text.nonEmpty(text) - - def optFirstLine(text: String): Option[PlainOneLine] = - Text.nonEmpty(Text.firstLineOf(text)) - - def firstLine(text: String, default: => String): PlainOneLine = - optFirstLine(text).getOrElse(default) - - def firstLineEmpty(text: String): PlainOneLine = firstLine(text, "") - - given string2FirstLineEmpty: Conversion[String, PlainOneLine] = - firstLineEmpty(_) - - extension (p: PlainOneLine) def toString: String = p - -opaque type Markdown = String - -object Markdown: - def apply(text: String): Validation[MessageId, Markdown] = - Text.validateNonEmpty(text) - - def opt(text: String): Validation[Nothing, Option[Markdown]] = - Validation.succeed(optDirect(text)) - - def optDirect(text: String): Option[Markdown] = - Text.nonEmpty(text) - - extension (p: Markdown) def toString: String = p diff --git a/build.sbt b/build.sbt index b2bdb10..50487b2 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ publishToIW lazy val core = crossProject(JSPlatform, JVMPlatform) - .crossType(CrossType.Pure) + .crossType(CrossType.Full) .settings(name := "iw-support-core") .in(file("core")) .settings(IWDeps.zioPrelude) diff --git a/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala new file mode 100644 index 0000000..640fb29 --- /dev/null +++ b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala @@ -0,0 +1,8 @@ +package works.iterative.core + +import scala.scalajs.js + +trait CzechSupportPlatformSpecific: + given czechOrdering: Ordering[String] with + def compare(x: String, y: String): Int = + x.asInstanceOf[js.Dynamic].localeCompare(y, "cs-CZ").asInstanceOf[Int] diff --git a/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala b/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala new file mode 100644 index 0000000..9f15ea8 --- /dev/null +++ b/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala @@ -0,0 +1,9 @@ +package works.iterative.core + +trait CzechSupportPlatformSpecific: + given czechOrdering: Ordering[String] = + Ordering.comparatorToOrdering( + java.text.Collator + .getInstance(java.util.Locale.forLanguageTag("cs-CZ")) + .asInstanceOf[java.util.Comparator[String]] + ) diff --git a/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala b/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala new file mode 100644 index 0000000..c174327 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala @@ -0,0 +1,6 @@ +package works.iterative.core + +trait CzechSupport: + given czechOrdering: Ordering[String] + +object CzechSupport extends CzechSupport with CzechSupportPlatformSpecific diff --git a/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala b/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala new file mode 100644 index 0000000..ac38528 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala @@ -0,0 +1,14 @@ +package works.iterative +package core + +// TODO: generic message catalogue +// we need to be able to render HTML messages +// like a list of items for example +trait MessageCatalogue: + def apply(id: MessageId): String = + get(id).getOrElse(id.toString()) + def apply(msg: UserMessage): String = + get(msg).getOrElse(msg.id.toString()) + + def get(id: MessageId): Option[String] + def get(msg: UserMessage): Option[String] diff --git a/core/shared/src/main/scala/works/iterative/core/MessageId.scala b/core/shared/src/main/scala/works/iterative/core/MessageId.scala new file mode 100644 index 0000000..72c4777 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/MessageId.scala @@ -0,0 +1,18 @@ +package works.iterative +package core + +// TODO: use refined's NonEmptyString after macros are ported to Scala 3 +// https://github.com/fthomas/refined/issues/932 +// We could than use the inlined macro to validate non empty strings at runtime + +/* MessageId is an opaque type to mark all keys to translations. + * The intent is to use it during build to generate a list of all keys + * and lately to check if we have all the translations we need + */ +opaque type MessageId = String + +object MessageId: + def apply(id: String): MessageId = id + + inline given Conversion[String, MessageId] with + inline def apply(id: String): MessageId = MessageId(id) diff --git a/core/shared/src/main/scala/works/iterative/core/Text.scala b/core/shared/src/main/scala/works/iterative/core/Text.scala new file mode 100644 index 0000000..cccaf59 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/Text.scala @@ -0,0 +1,108 @@ +package works.iterative +package core + +import scala.jdk.OptionConverters.given +import zio.prelude.Validation + +/* UserText represents different, more specific variants of String to be able to + * better identify what kind of String are we looking It is meant to be used + * primarily for UI, but sometimes it is useful to store the different text + * representations variantly, eg. it is useful to have the distinction even on + * the level of the data model, not just in the UI + */ +object Text: + + def nonEmpty(s: String): Option[String] = Option(s).filterNot(_.trim.isEmpty) + + def validateNonEmpty[T](text: T)(using + ev: T =:= String + ): Validation[MessageId, T] = + Validation.fromPredicateWith[MessageId, T]( + "validation.text.empty" + )(text)(t => ev(t).nonEmpty) + + def firstNewLine(t: String): Int = + val lf = t.indexOf('\n') + def cr = t.indexOf('\r') + if lf != -1 then lf else cr + + def hasNewLine(t: String): Boolean = firstNewLine(t) != -1 + + def firstLineOf(t: String): String = + firstNewLine(t) match + case -1 => t + case i => t.take(i) + +opaque type PlainMultiLine = String + +object PlainMultiLine: + def apply(text: String): Validation[MessageId, PlainMultiLine] = + Text.validateNonEmpty(text) + + def opt(text: String): Validation[Nothing, Option[PlainMultiLine]] = + Validation.succeed(optDirect(text)) + + def optDirect(text: String): Option[PlainMultiLine] = + Text.nonEmpty(text) + + given string2plainMultiline: Conversion[String, Option[PlainMultiLine]] with + def apply(text: String): Option[PlainMultiLine] = optDirect(text) + + given optString2PlainMultiline + : Conversion[Option[String], Option[PlainMultiLine]] with + def apply(text: Option[String]): Option[PlainMultiLine] = + text.flatMap(optDirect) + + given plainMultiLine2String: Conversion[PlainMultiLine, String] with + def apply(p: PlainMultiLine): String = p.toString + + given optionPlainMultiLine2OptionString + : Conversion[Option[PlainMultiLine], Option[String]] with + def apply(p: Option[PlainMultiLine]): Option[String] = p.map(_.toString) + + extension (p: PlainMultiLine) def toString: String = p + +opaque type PlainOneLine = String + +object PlainOneLine: + def validateOneLine(text: PlainOneLine): Validation[MessageId, PlainOneLine] = + Validation.fromPredicateWith[MessageId, PlainOneLine]( + "validation.text.oneline" + )(text)(!Text.hasNewLine(_)) + + def apply(text: String): Validation[MessageId, PlainOneLine] = + for + _ <- Text.validateNonEmpty(text) + _ <- validateOneLine(text) + yield text + + def opt(text: String): Validation[MessageId, Option[PlainOneLine]] = + for _ <- validateOneLine(text) + yield Text.nonEmpty(text) + + def optFirstLine(text: String): Option[PlainOneLine] = + Text.nonEmpty(Text.firstLineOf(text)) + + def firstLine(text: String, default: => String): PlainOneLine = + optFirstLine(text).getOrElse(default) + + def firstLineEmpty(text: String): PlainOneLine = firstLine(text, "") + + given string2FirstLineEmpty: Conversion[String, PlainOneLine] = + firstLineEmpty(_) + + extension (p: PlainOneLine) def toString: String = p + +opaque type Markdown = String + +object Markdown: + def apply(text: String): Validation[MessageId, Markdown] = + Text.validateNonEmpty(text) + + def opt(text: String): Validation[Nothing, Option[Markdown]] = + Validation.succeed(optDirect(text)) + + def optDirect(text: String): Option[Markdown] = + Text.nonEmpty(text) + + extension (p: Markdown) def toString: String = p diff --git a/core/shared/src/main/scala/works/iterative/core/UserMessage.scala b/core/shared/src/main/scala/works/iterative/core/UserMessage.scala new file mode 100644 index 0000000..d2d9343 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/UserMessage.scala @@ -0,0 +1,6 @@ +package works.iterative +package core + +// Type-wise naive solution to speicifying user messages. +// A mechanism that will check the message for correct formatting and validate parameters is needed +case class UserMessage(id: MessageId, args: Any*) diff --git a/core/src/main/scala/works/iterative/core/MessageCatalogue.scala b/core/src/main/scala/works/iterative/core/MessageCatalogue.scala deleted file mode 100644 index ac38528..0000000 --- a/core/src/main/scala/works/iterative/core/MessageCatalogue.scala +++ /dev/null @@ -1,14 +0,0 @@ -package works.iterative -package core - -// TODO: generic message catalogue -// we need to be able to render HTML messages -// like a list of items for example -trait MessageCatalogue: - def apply(id: MessageId): String = - get(id).getOrElse(id.toString()) - def apply(msg: UserMessage): String = - get(msg).getOrElse(msg.id.toString()) - - def get(id: MessageId): Option[String] - def get(msg: UserMessage): Option[String] diff --git a/core/src/main/scala/works/iterative/core/MessageId.scala b/core/src/main/scala/works/iterative/core/MessageId.scala deleted file mode 100644 index 72c4777..0000000 --- a/core/src/main/scala/works/iterative/core/MessageId.scala +++ /dev/null @@ -1,18 +0,0 @@ -package works.iterative -package core - -// TODO: use refined's NonEmptyString after macros are ported to Scala 3 -// https://github.com/fthomas/refined/issues/932 -// We could than use the inlined macro to validate non empty strings at runtime - -/* MessageId is an opaque type to mark all keys to translations. - * The intent is to use it during build to generate a list of all keys - * and lately to check if we have all the translations we need - */ -opaque type MessageId = String - -object MessageId: - def apply(id: String): MessageId = id - - inline given Conversion[String, MessageId] with - inline def apply(id: String): MessageId = MessageId(id) diff --git a/core/src/main/scala/works/iterative/core/Text.scala b/core/src/main/scala/works/iterative/core/Text.scala deleted file mode 100644 index cccaf59..0000000 --- a/core/src/main/scala/works/iterative/core/Text.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative -package core - -import scala.jdk.OptionConverters.given -import zio.prelude.Validation - -/* UserText represents different, more specific variants of String to be able to - * better identify what kind of String are we looking It is meant to be used - * primarily for UI, but sometimes it is useful to store the different text - * representations variantly, eg. it is useful to have the distinction even on - * the level of the data model, not just in the UI - */ -object Text: - - def nonEmpty(s: String): Option[String] = Option(s).filterNot(_.trim.isEmpty) - - def validateNonEmpty[T](text: T)(using - ev: T =:= String - ): Validation[MessageId, T] = - Validation.fromPredicateWith[MessageId, T]( - "validation.text.empty" - )(text)(t => ev(t).nonEmpty) - - def firstNewLine(t: String): Int = - val lf = t.indexOf('\n') - def cr = t.indexOf('\r') - if lf != -1 then lf else cr - - def hasNewLine(t: String): Boolean = firstNewLine(t) != -1 - - def firstLineOf(t: String): String = - firstNewLine(t) match - case -1 => t - case i => t.take(i) - -opaque type PlainMultiLine = String - -object PlainMultiLine: - def apply(text: String): Validation[MessageId, PlainMultiLine] = - Text.validateNonEmpty(text) - - def opt(text: String): Validation[Nothing, Option[PlainMultiLine]] = - Validation.succeed(optDirect(text)) - - def optDirect(text: String): Option[PlainMultiLine] = - Text.nonEmpty(text) - - given string2plainMultiline: Conversion[String, Option[PlainMultiLine]] with - def apply(text: String): Option[PlainMultiLine] = optDirect(text) - - given optString2PlainMultiline - : Conversion[Option[String], Option[PlainMultiLine]] with - def apply(text: Option[String]): Option[PlainMultiLine] = - text.flatMap(optDirect) - - given plainMultiLine2String: Conversion[PlainMultiLine, String] with - def apply(p: PlainMultiLine): String = p.toString - - given optionPlainMultiLine2OptionString - : Conversion[Option[PlainMultiLine], Option[String]] with - def apply(p: Option[PlainMultiLine]): Option[String] = p.map(_.toString) - - extension (p: PlainMultiLine) def toString: String = p - -opaque type PlainOneLine = String - -object PlainOneLine: - def validateOneLine(text: PlainOneLine): Validation[MessageId, PlainOneLine] = - Validation.fromPredicateWith[MessageId, PlainOneLine]( - "validation.text.oneline" - )(text)(!Text.hasNewLine(_)) - - def apply(text: String): Validation[MessageId, PlainOneLine] = - for - _ <- Text.validateNonEmpty(text) - _ <- validateOneLine(text) - yield text - - def opt(text: String): Validation[MessageId, Option[PlainOneLine]] = - for _ <- validateOneLine(text) - yield Text.nonEmpty(text) - - def optFirstLine(text: String): Option[PlainOneLine] = - Text.nonEmpty(Text.firstLineOf(text)) - - def firstLine(text: String, default: => String): PlainOneLine = - optFirstLine(text).getOrElse(default) - - def firstLineEmpty(text: String): PlainOneLine = firstLine(text, "") - - given string2FirstLineEmpty: Conversion[String, PlainOneLine] = - firstLineEmpty(_) - - extension (p: PlainOneLine) def toString: String = p - -opaque type Markdown = String - -object Markdown: - def apply(text: String): Validation[MessageId, Markdown] = - Text.validateNonEmpty(text) - - def opt(text: String): Validation[Nothing, Option[Markdown]] = - Validation.succeed(optDirect(text)) - - def optDirect(text: String): Option[Markdown] = - Text.nonEmpty(text) - - extension (p: Markdown) def toString: String = p diff --git a/core/src/main/scala/works/iterative/core/UserMessage.scala b/core/src/main/scala/works/iterative/core/UserMessage.scala deleted file mode 100644 index d2d9343..0000000 --- a/core/src/main/scala/works/iterative/core/UserMessage.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative -package core - -// Type-wise naive solution to speicifying user messages. -// A mechanism that will check the message for correct formatting and validate parameters is needed -case class UserMessage(id: MessageId, args: Any*) diff --git a/build.sbt b/build.sbt index b2bdb10..50487b2 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ publishToIW lazy val core = crossProject(JSPlatform, JVMPlatform) - .crossType(CrossType.Pure) + .crossType(CrossType.Full) .settings(name := "iw-support-core") .in(file("core")) .settings(IWDeps.zioPrelude) diff --git a/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala new file mode 100644 index 0000000..640fb29 --- /dev/null +++ b/core/js/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala @@ -0,0 +1,8 @@ +package works.iterative.core + +import scala.scalajs.js + +trait CzechSupportPlatformSpecific: + given czechOrdering: Ordering[String] with + def compare(x: String, y: String): Int = + x.asInstanceOf[js.Dynamic].localeCompare(y, "cs-CZ").asInstanceOf[Int] diff --git a/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala b/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala new file mode 100644 index 0000000..9f15ea8 --- /dev/null +++ b/core/jvm/src/main/scala/works/iterative/core/CzechSupportPlatformSpecific.scala @@ -0,0 +1,9 @@ +package works.iterative.core + +trait CzechSupportPlatformSpecific: + given czechOrdering: Ordering[String] = + Ordering.comparatorToOrdering( + java.text.Collator + .getInstance(java.util.Locale.forLanguageTag("cs-CZ")) + .asInstanceOf[java.util.Comparator[String]] + ) diff --git a/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala b/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala new file mode 100644 index 0000000..c174327 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/CzechSupport.scala @@ -0,0 +1,6 @@ +package works.iterative.core + +trait CzechSupport: + given czechOrdering: Ordering[String] + +object CzechSupport extends CzechSupport with CzechSupportPlatformSpecific diff --git a/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala b/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala new file mode 100644 index 0000000..ac38528 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/MessageCatalogue.scala @@ -0,0 +1,14 @@ +package works.iterative +package core + +// TODO: generic message catalogue +// we need to be able to render HTML messages +// like a list of items for example +trait MessageCatalogue: + def apply(id: MessageId): String = + get(id).getOrElse(id.toString()) + def apply(msg: UserMessage): String = + get(msg).getOrElse(msg.id.toString()) + + def get(id: MessageId): Option[String] + def get(msg: UserMessage): Option[String] diff --git a/core/shared/src/main/scala/works/iterative/core/MessageId.scala b/core/shared/src/main/scala/works/iterative/core/MessageId.scala new file mode 100644 index 0000000..72c4777 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/MessageId.scala @@ -0,0 +1,18 @@ +package works.iterative +package core + +// TODO: use refined's NonEmptyString after macros are ported to Scala 3 +// https://github.com/fthomas/refined/issues/932 +// We could than use the inlined macro to validate non empty strings at runtime + +/* MessageId is an opaque type to mark all keys to translations. + * The intent is to use it during build to generate a list of all keys + * and lately to check if we have all the translations we need + */ +opaque type MessageId = String + +object MessageId: + def apply(id: String): MessageId = id + + inline given Conversion[String, MessageId] with + inline def apply(id: String): MessageId = MessageId(id) diff --git a/core/shared/src/main/scala/works/iterative/core/Text.scala b/core/shared/src/main/scala/works/iterative/core/Text.scala new file mode 100644 index 0000000..cccaf59 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/Text.scala @@ -0,0 +1,108 @@ +package works.iterative +package core + +import scala.jdk.OptionConverters.given +import zio.prelude.Validation + +/* UserText represents different, more specific variants of String to be able to + * better identify what kind of String are we looking It is meant to be used + * primarily for UI, but sometimes it is useful to store the different text + * representations variantly, eg. it is useful to have the distinction even on + * the level of the data model, not just in the UI + */ +object Text: + + def nonEmpty(s: String): Option[String] = Option(s).filterNot(_.trim.isEmpty) + + def validateNonEmpty[T](text: T)(using + ev: T =:= String + ): Validation[MessageId, T] = + Validation.fromPredicateWith[MessageId, T]( + "validation.text.empty" + )(text)(t => ev(t).nonEmpty) + + def firstNewLine(t: String): Int = + val lf = t.indexOf('\n') + def cr = t.indexOf('\r') + if lf != -1 then lf else cr + + def hasNewLine(t: String): Boolean = firstNewLine(t) != -1 + + def firstLineOf(t: String): String = + firstNewLine(t) match + case -1 => t + case i => t.take(i) + +opaque type PlainMultiLine = String + +object PlainMultiLine: + def apply(text: String): Validation[MessageId, PlainMultiLine] = + Text.validateNonEmpty(text) + + def opt(text: String): Validation[Nothing, Option[PlainMultiLine]] = + Validation.succeed(optDirect(text)) + + def optDirect(text: String): Option[PlainMultiLine] = + Text.nonEmpty(text) + + given string2plainMultiline: Conversion[String, Option[PlainMultiLine]] with + def apply(text: String): Option[PlainMultiLine] = optDirect(text) + + given optString2PlainMultiline + : Conversion[Option[String], Option[PlainMultiLine]] with + def apply(text: Option[String]): Option[PlainMultiLine] = + text.flatMap(optDirect) + + given plainMultiLine2String: Conversion[PlainMultiLine, String] with + def apply(p: PlainMultiLine): String = p.toString + + given optionPlainMultiLine2OptionString + : Conversion[Option[PlainMultiLine], Option[String]] with + def apply(p: Option[PlainMultiLine]): Option[String] = p.map(_.toString) + + extension (p: PlainMultiLine) def toString: String = p + +opaque type PlainOneLine = String + +object PlainOneLine: + def validateOneLine(text: PlainOneLine): Validation[MessageId, PlainOneLine] = + Validation.fromPredicateWith[MessageId, PlainOneLine]( + "validation.text.oneline" + )(text)(!Text.hasNewLine(_)) + + def apply(text: String): Validation[MessageId, PlainOneLine] = + for + _ <- Text.validateNonEmpty(text) + _ <- validateOneLine(text) + yield text + + def opt(text: String): Validation[MessageId, Option[PlainOneLine]] = + for _ <- validateOneLine(text) + yield Text.nonEmpty(text) + + def optFirstLine(text: String): Option[PlainOneLine] = + Text.nonEmpty(Text.firstLineOf(text)) + + def firstLine(text: String, default: => String): PlainOneLine = + optFirstLine(text).getOrElse(default) + + def firstLineEmpty(text: String): PlainOneLine = firstLine(text, "") + + given string2FirstLineEmpty: Conversion[String, PlainOneLine] = + firstLineEmpty(_) + + extension (p: PlainOneLine) def toString: String = p + +opaque type Markdown = String + +object Markdown: + def apply(text: String): Validation[MessageId, Markdown] = + Text.validateNonEmpty(text) + + def opt(text: String): Validation[Nothing, Option[Markdown]] = + Validation.succeed(optDirect(text)) + + def optDirect(text: String): Option[Markdown] = + Text.nonEmpty(text) + + extension (p: Markdown) def toString: String = p diff --git a/core/shared/src/main/scala/works/iterative/core/UserMessage.scala b/core/shared/src/main/scala/works/iterative/core/UserMessage.scala new file mode 100644 index 0000000..d2d9343 --- /dev/null +++ b/core/shared/src/main/scala/works/iterative/core/UserMessage.scala @@ -0,0 +1,6 @@ +package works.iterative +package core + +// Type-wise naive solution to speicifying user messages. +// A mechanism that will check the message for correct formatting and validate parameters is needed +case class UserMessage(id: MessageId, args: Any*) diff --git a/core/src/main/scala/works/iterative/core/MessageCatalogue.scala b/core/src/main/scala/works/iterative/core/MessageCatalogue.scala deleted file mode 100644 index ac38528..0000000 --- a/core/src/main/scala/works/iterative/core/MessageCatalogue.scala +++ /dev/null @@ -1,14 +0,0 @@ -package works.iterative -package core - -// TODO: generic message catalogue -// we need to be able to render HTML messages -// like a list of items for example -trait MessageCatalogue: - def apply(id: MessageId): String = - get(id).getOrElse(id.toString()) - def apply(msg: UserMessage): String = - get(msg).getOrElse(msg.id.toString()) - - def get(id: MessageId): Option[String] - def get(msg: UserMessage): Option[String] diff --git a/core/src/main/scala/works/iterative/core/MessageId.scala b/core/src/main/scala/works/iterative/core/MessageId.scala deleted file mode 100644 index 72c4777..0000000 --- a/core/src/main/scala/works/iterative/core/MessageId.scala +++ /dev/null @@ -1,18 +0,0 @@ -package works.iterative -package core - -// TODO: use refined's NonEmptyString after macros are ported to Scala 3 -// https://github.com/fthomas/refined/issues/932 -// We could than use the inlined macro to validate non empty strings at runtime - -/* MessageId is an opaque type to mark all keys to translations. - * The intent is to use it during build to generate a list of all keys - * and lately to check if we have all the translations we need - */ -opaque type MessageId = String - -object MessageId: - def apply(id: String): MessageId = id - - inline given Conversion[String, MessageId] with - inline def apply(id: String): MessageId = MessageId(id) diff --git a/core/src/main/scala/works/iterative/core/Text.scala b/core/src/main/scala/works/iterative/core/Text.scala deleted file mode 100644 index cccaf59..0000000 --- a/core/src/main/scala/works/iterative/core/Text.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative -package core - -import scala.jdk.OptionConverters.given -import zio.prelude.Validation - -/* UserText represents different, more specific variants of String to be able to - * better identify what kind of String are we looking It is meant to be used - * primarily for UI, but sometimes it is useful to store the different text - * representations variantly, eg. it is useful to have the distinction even on - * the level of the data model, not just in the UI - */ -object Text: - - def nonEmpty(s: String): Option[String] = Option(s).filterNot(_.trim.isEmpty) - - def validateNonEmpty[T](text: T)(using - ev: T =:= String - ): Validation[MessageId, T] = - Validation.fromPredicateWith[MessageId, T]( - "validation.text.empty" - )(text)(t => ev(t).nonEmpty) - - def firstNewLine(t: String): Int = - val lf = t.indexOf('\n') - def cr = t.indexOf('\r') - if lf != -1 then lf else cr - - def hasNewLine(t: String): Boolean = firstNewLine(t) != -1 - - def firstLineOf(t: String): String = - firstNewLine(t) match - case -1 => t - case i => t.take(i) - -opaque type PlainMultiLine = String - -object PlainMultiLine: - def apply(text: String): Validation[MessageId, PlainMultiLine] = - Text.validateNonEmpty(text) - - def opt(text: String): Validation[Nothing, Option[PlainMultiLine]] = - Validation.succeed(optDirect(text)) - - def optDirect(text: String): Option[PlainMultiLine] = - Text.nonEmpty(text) - - given string2plainMultiline: Conversion[String, Option[PlainMultiLine]] with - def apply(text: String): Option[PlainMultiLine] = optDirect(text) - - given optString2PlainMultiline - : Conversion[Option[String], Option[PlainMultiLine]] with - def apply(text: Option[String]): Option[PlainMultiLine] = - text.flatMap(optDirect) - - given plainMultiLine2String: Conversion[PlainMultiLine, String] with - def apply(p: PlainMultiLine): String = p.toString - - given optionPlainMultiLine2OptionString - : Conversion[Option[PlainMultiLine], Option[String]] with - def apply(p: Option[PlainMultiLine]): Option[String] = p.map(_.toString) - - extension (p: PlainMultiLine) def toString: String = p - -opaque type PlainOneLine = String - -object PlainOneLine: - def validateOneLine(text: PlainOneLine): Validation[MessageId, PlainOneLine] = - Validation.fromPredicateWith[MessageId, PlainOneLine]( - "validation.text.oneline" - )(text)(!Text.hasNewLine(_)) - - def apply(text: String): Validation[MessageId, PlainOneLine] = - for - _ <- Text.validateNonEmpty(text) - _ <- validateOneLine(text) - yield text - - def opt(text: String): Validation[MessageId, Option[PlainOneLine]] = - for _ <- validateOneLine(text) - yield Text.nonEmpty(text) - - def optFirstLine(text: String): Option[PlainOneLine] = - Text.nonEmpty(Text.firstLineOf(text)) - - def firstLine(text: String, default: => String): PlainOneLine = - optFirstLine(text).getOrElse(default) - - def firstLineEmpty(text: String): PlainOneLine = firstLine(text, "") - - given string2FirstLineEmpty: Conversion[String, PlainOneLine] = - firstLineEmpty(_) - - extension (p: PlainOneLine) def toString: String = p - -opaque type Markdown = String - -object Markdown: - def apply(text: String): Validation[MessageId, Markdown] = - Text.validateNonEmpty(text) - - def opt(text: String): Validation[Nothing, Option[Markdown]] = - Validation.succeed(optDirect(text)) - - def optDirect(text: String): Option[Markdown] = - Text.nonEmpty(text) - - extension (p: Markdown) def toString: String = p diff --git a/core/src/main/scala/works/iterative/core/UserMessage.scala b/core/src/main/scala/works/iterative/core/UserMessage.scala deleted file mode 100644 index d2d9343..0000000 --- a/core/src/main/scala/works/iterative/core/UserMessage.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative -package core - -// Type-wise naive solution to speicifying user messages. -// A mechanism that will check the message for correct formatting and validate parameters is needed -case class UserMessage(id: MessageId, args: Any*) diff --git a/ui/components/src/main/scala/works/iterative/services/files/components/tailwind/FileTable.scala b/ui/components/src/main/scala/works/iterative/services/files/components/tailwind/FileTable.scala index 9d75795..c665744 100644 --- a/ui/components/src/main/scala/works/iterative/services/files/components/tailwind/FileTable.scala +++ b/ui/components/src/main/scala/works/iterative/services/files/components/tailwind/FileTable.scala @@ -9,6 +9,7 @@ import java.time.ZoneId import java.util.Locale import works.iterative.ui.components.tailwind.TimeUtils +import works.iterative.core.CzechSupport def FileTable( files: Signal[List[File]], @@ -95,7 +96,8 @@ ), name ) - ) :: (if o.contains(name) then files.sortBy(_.name).map(renderRow) + ) :: (if o.contains(name) then + files.sortBy(_.name)(CzechSupport.czechOrdering).map(renderRow) else Nil) ) @@ -130,7 +132,7 @@ .combineSeq( f.groupBy(_.category) .to(List) - .sortBy(_._1) + .sortBy(_._1)(CzechSupport.czechOrdering) .map(renderCategory(_, _)) ) .map(_.flatten)