feat: project seperation

This commit is contained in:
DavidOnTop 2024-09-20 22:02:05 +02:00
parent a679aeb978
commit 3e06f86735
No known key found for this signature in database
GPG key ID: 5D05538A45D5149F
24 changed files with 49 additions and 21 deletions

View file

@ -0,0 +1,18 @@
package top.davidon.sfs.dom.defs.complex
import top.davidon.sfs.dom.codecs
import top.davidon.sfs.dom.keys.HtmlAttr
trait ComplexHtmlKeys {
lazy val rel = HtmlAttr("rel", codecs.StringAsIsCodec)
lazy val role = HtmlAttr("role", codecs.StringAsIsCodec)
lazy val styleAttr = HtmlAttr("style", codecs.StringAsIsCodec)
val className = HtmlAttr("class", codecs.StringAsIsCodec)
val cls: HtmlAttr[String] = className
val `class`: HtmlAttr[String] = className
val classList = HtmlAttr("class", codecs.IterableAsSpaceSeparatedStringCodec)
val cl: HtmlAttr[Iterable[String]] = classList
def dataAttr(suffix: String): HtmlAttr[String] =
HtmlAttr[String]("data-" + suffix, codecs.StringAsIsCodec)
}

View file

@ -0,0 +1,13 @@
package top.davidon.sfs.dom.defs.complex
import top.davidon.sfs.dom.codecs
import top.davidon.sfs.dom.keys.HtmlAttr
trait ComplexSvgKeys {
lazy val role = HtmlAttr("role", codecs.StringAsIsCodec)
val className = HtmlAttr("class", codecs.StringAsIsCodec)
val cls: HtmlAttr[String] = className
val `class`: HtmlAttr[String] = className
val classList = HtmlAttr("class", codecs.IterableAsSpaceSeparatedStringCodec)
val cl: HtmlAttr[Iterable[String]] = classList
}

View file

@ -0,0 +1,10 @@
package top.davidon.sfs.dom
import top.davidon.sfs.dom.tags.Tag
/** tag + modifiers + value */
class Element[+Ref <: org.scalajs.dom.Element](
val tag: Tag[Ref],
val mods: Iterable[Modifier[?, ?]],
val value: Value[?, String]
) {}

View file

@ -0,0 +1,6 @@
package top.davidon.sfs.dom
import top.davidon.sfs.dom.Value
import top.davidon.sfs.dom.keys.Key
class Modifier[F, T](val key: Key, val value: Value[F, T]) {}

View file

@ -0,0 +1,7 @@
package top.davidon.sfs.dom
import org.scalajs.dom
trait Renderer[T] {
def render(elements: Element[dom.Element]*): T
}

View file

@ -0,0 +1,3 @@
package top.davidon.sfs.dom
object SFS extends ScalaFullStack {}

View file

@ -0,0 +1,58 @@
package top.davidon.sfs.dom
import top.davidon.sfs.dom.codecs.*
import top.davidon.sfs.dom.defs.attrs.{AriaAttrs, HtmlAttrs, SvgAttrs}
import top.davidon.sfs.dom.defs.complex.{ComplexHtmlKeys, ComplexSvgKeys}
import top.davidon.sfs.dom.defs.eventProps.GlobalEventProps
import top.davidon.sfs.dom.defs.props.HtmlProps
import top.davidon.sfs.dom.defs.tags.{HtmlTags, SvgTags}
trait ScalaFullStack
extends HtmlTags
with HtmlAttrs
with HtmlProps
with GlobalEventProps
with ComplexHtmlKeys {
type Component = Element[?]
object aria extends AriaAttrs
object svg extends SvgTags with SvgAttrs with ComplexSvgKeys
given AsValue[String, String] with {
extension (from: String) {
def asStringValue(): Value[String, String] = {
Value(from, StringAsIsCodec)
}
}
}
given AsValue[Int, String] with {
extension (from: Int) {
def asStringValue(): Value[Int, String] = {
Value(from, IntAsStringCodec)
}
}
}
given AsValue[Double, String] with {
extension (from: Double) {
def asStringValue(): Value[Double, String] = {
Value(from, DoubleAsStringCodec)
}
}
}
given AsValue[Boolean, String] with {
extension (from: Boolean) {
def asStringValue(): Value[Boolean, String] = {
Value(from, BooleanAsTrueFalseStringCodec)
}
}
}
given AsValue[Iterable[String], String] with {
extension (from: Iterable[String]) {
def asStringValue(): Value[Iterable[String], String] = {
Value(from, IterableAsSpaceSeparatedStringCodec)
}
}
}
}

View file

@ -0,0 +1,37 @@
package top.davidon.sfs.dom
import top.davidon.sfs.dom.codecs.{Codec, StringAsIsCodec}
class Value[F, T](
val value: F,
val codec: Codec[F, T],
var isReactive: Boolean = false
) {
def apply(): T = {
codec.encode(value)
}
def reactive(value: Boolean = true): Value[F, T] = {
isReactive = value
this
}
}
trait AsValue[F, T] {
extension (from: F) {
def asStringValue(): Value[F, String]
}
}
object Value {
def join(
iterator: Iterable[Value[?, String]]
): Value[String, String] = {
Value(
iterator.map(v => v.codec.encode(v.value)).mkString(""),
StringAsIsCodec,
iterator.exists(_.isReactive)
)
}
}

View file

@ -0,0 +1,25 @@
package top.davidon.sfs.dom.codecs
trait Codec[ScalaType, DomType] {
/** Convert the result of a `dom.Node.getAttribute` call to appropriate Scala
* type.
*
* Note: HTML Attributes are generally optional, and `dom.Node.getAttribute`
* will return `null` if an attribute is not defined on a given DOM node.
* However, this decoder is only intended for cases when the attribute is
* defined.
*/
def decode(domValue: DomType): ScalaType
/** Convert desired attribute value to appropriate DOM type. The resulting
* value should be passed to `dom.Node.setAttribute` call, EXCEPT when
* resulting value is a `null`. In that case you should call
* `dom.Node.removeAttribute` instead.
*
* We use `null` instead of [[Option]] here to reduce overhead in JS land.
* This method should not be called by end users anyway, it's the consuming
* library's job to call this method under the hood.
*/
def encode(scalaValue: ScalaType): DomType
}

View file

@ -0,0 +1,87 @@
package top.davidon.sfs.dom
package object codecs {
lazy val IntAsStringCodec: Codec[Int, String] = new Codec[Int, String] {
override def decode(domValue: String): Int =
domValue.toInt // @TODO this can throw exception. How do we handle this?
override def encode(scalaValue: Int): String = scalaValue.toString
}
lazy val DoubleAsIsCodec: Codec[Double, Double] = AsIsCodec()
lazy val DoubleAsStringCodec: Codec[Double, String] =
new Codec[Double, String] {
override def decode(domValue: String): Double =
domValue.toDouble // @TODO this can throw exception. How do we handle this?
override def encode(scalaValue: Double): String = scalaValue.toString
}
lazy val BooleanAsTrueFalseStringCodec: Codec[Boolean, String] =
new Codec[Boolean, String] {
override def decode(domValue: String): Boolean = domValue == "true"
override def encode(scalaValue: Boolean): String =
if scalaValue then "true" else "false"
}
lazy val BooleanAsYesNoStringCodec: Codec[Boolean, String] =
new Codec[Boolean, String] {
override def decode(domValue: String): Boolean = domValue == "yes"
override def encode(scalaValue: Boolean): String =
if scalaValue then "yes" else "no"
}
lazy val BooleanAsOnOffStringCodec: Codec[Boolean, String] =
new Codec[Boolean, String] {
override def decode(domValue: String): Boolean = domValue == "on"
override def encode(scalaValue: Boolean): String =
if scalaValue then "on" else "off"
}
lazy val IterableAsSpaceSeparatedStringCodec
: Codec[Iterable[String], String] =
new Codec[Iterable[String], String] { // could use for e.g. className
override def decode(domValue: String): Iterable[String] =
if domValue == "" then Nil else domValue.split(' ')
override def encode(scalaValue: Iterable[String]): String =
scalaValue.mkString(" ")
}
lazy val IterableAsCommaSeparatedStringCodec
: Codec[Iterable[String], String] =
new Codec[Iterable[String], String] { // could use for lists of IDs
override def decode(domValue: String): Iterable[String] =
if domValue == "" then Nil else domValue.split(',')
override def encode(scalaValue: Iterable[String]): String =
scalaValue.mkString(",")
}
val StringAsIsCodec: Codec[String, String] = AsIsCodec()
val IntAsIsCodec: Codec[Int, Int] = AsIsCodec()
val BooleanAsIsCodec: Codec[Boolean, Boolean] = AsIsCodec()
val BooleanAsAttrPresenceCodec: Codec[Boolean, String] =
new Codec[Boolean, String] {
override def decode(domValue: String): Boolean = domValue != null
override def encode(scalaValue: Boolean): String =
if scalaValue then "" else null
}
def AsIsCodec[V](): Codec[V, V] = new Codec[V, V] {
override def encode(scalaValue: V): V = scalaValue
override def decode(domValue: V): V = domValue
}
}

View file

@ -0,0 +1,18 @@
package top.davidon.sfs.dom.keys
import top.davidon.sfs.dom.codecs.Codec
import top.davidon.sfs.dom.{Modifier, Value}
class AriaAttr[V](
suffix: String,
val codec: Codec[V, String]
) extends Key {
override val name: String = "aria-" + suffix
@inline def apply(value: V): Modifier[V, String] = {
this := value
}
def :=(value: V): Modifier[V, String] = {
Modifier(this, Value(value, codec))
}
}

View file

@ -0,0 +1,5 @@
package top.davidon.sfs.dom.keys
import org.scalajs.dom
class EventProp[Event <: dom.Event](override val name: String) extends Key {}

View file

@ -0,0 +1,17 @@
package top.davidon.sfs.dom.keys
import top.davidon.sfs.dom.codecs.Codec
import top.davidon.sfs.dom.{Modifier, Value}
class HtmlAttr[V](
override val name: String,
val codec: Codec[V, String]
) extends Key {
@inline def apply(value: V): Modifier[V, String] = {
this := value
}
def :=(value: V): Modifier[V, String] = {
Modifier(this, Value(value, codec))
}
}

View file

@ -0,0 +1,16 @@
package top.davidon.sfs.dom.keys
import top.davidon.sfs.dom.codecs.Codec
import top.davidon.sfs.dom.{Modifier, Value}
class HtmlProp[V, DomV](
override val name: String,
val codec: Codec[V, DomV]
) extends Key {
@inline def apply(value: V): Modifier[V, DomV] = {
this := value
}
def :=(value: V): Modifier[V, DomV] = {
Modifier(this, Value(value, codec))
}
}

View file

@ -0,0 +1,5 @@
package top.davidon.sfs.dom.keys
abstract class Key {
val name: String
}

View file

@ -0,0 +1,38 @@
package top.davidon.sfs.dom.keys
import top.davidon.sfs.dom.codecs.Codec
import top.davidon.sfs.dom.{Modifier, Value}
class SvgAttr[V](
val localName: String,
val codec: Codec[V, String],
val namespacePrefix: Option[String]
) extends Key {
override val name: String =
namespacePrefix.map(_ + ":" + localName).getOrElse(localName)
val namespaceUri: Option[String] = namespacePrefix.map(SvgAttr.namespaceUri)
@inline def apply(value: V): Modifier[V, String] = {
this := value
}
def :=(value: V): Modifier[V, String] = {
Modifier(this, Value(value, codec))
}
}
object SvgAttr {
final val svgNamespaceUri: String = "http://www.w3.org/2000/svg"
final val xlinkNamespaceUri: String = "http://www.w3.org/1999/xlink"
final val xmlNamespaceUri: String = "http://www.w3.org/XML/1998/namespace"
final val xmlnsNamespaceUri: String = "http://www.w3.org/2000/xmlns/"
final def namespaceUri(namespace: String): String = {
namespace match {
case "svg" => svgNamespaceUri
case "xlink" => xlinkNamespaceUri
case "xml" => xmlNamespaceUri
case "xmlns" => xmlnsNamespaceUri
}
}
}

View file

@ -0,0 +1,6 @@
package top.davidon.sfs.dom.tags
class HtmlTag[+Ref <: org.scalajs.dom.html.Element](
override val name: String,
override val void: Boolean = false
) extends Tag[Ref] {}

View file

@ -0,0 +1,6 @@
package top.davidon.sfs.dom.tags
class SvgTag[+Ref <: org.scalajs.dom.svg.Element](
override val name: String,
override val void: Boolean = false
) extends Tag[Ref] {}

View file

@ -0,0 +1,17 @@
package top.davidon.sfs.dom.tags
import org.scalajs.dom
import top.davidon.sfs.dom.{Element, Modifier, Value}
trait Tag[+Ref <: dom.Element] {
val name: String
val void: Boolean
def apply(
modifiers: Modifier[?, ?]*
)(
values: Value[?, String]*
): Element[Ref] = {
Element[Ref](this, modifiers, Value.join(values))
}
}