diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cbd0275 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.gradle +build +.idea +local.properties +gradle.properties diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..102101d --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,138 @@ +plugins { + kotlin("multiplatform") version "1.5.31" + `maven-publish` + signing +} + +group = "nl.astraeus" +version = "1.0.0" + +repositories { + mavenLocal() + mavenCentral() + maven { setUrl("https://dl.bintray.com/kotlin/kotlin-eap") } +} + +kotlin { + jvm() + js(BOTH) { + browser { + testTask { + useKarma { + useFirefox() + //useChrome() + } + } + } + } + + sourceSets { + val commonMain by getting {} + val commonTest by getting { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + } + val jvmTest by getting { + dependencies { + implementation(kotlin("test-junit")) + } + } + val jsTest by getting { + dependencies { + implementation(kotlin("test-js")) + } + } + } +} + +extra["PUBLISH_GROUP_ID"] = "nl.astraeus" +extra["PUBLISH_VERSION"] = "1.0.0" +extra["PUBLISH_ARTIFACT_ID"] = "kotlin-css-generator" + +// Stub secrets to let the project sync and build without the publication values set up +val signingKeyId: String by project +val signingPassword: String by project +val signingSecretKeyRingFile: String by project +val ossrhUsername: String by project +val ossrhPassword: String by project + +extra["signing.keyId"] = signingKeyId +extra["signing.password"] = signingPassword +extra["signing.secretKeyRingFile"] = signingSecretKeyRingFile +extra["ossrhUsername"] = ossrhUsername +extra["ossrhPassword"] = ossrhPassword + +publishing { + repositories { + maven { + name = "releases" + // change to point to your repo, e.g. http://my.org/repo + url = uri("http://nexus.astraeus.nl/nexus/content/repositories/releases") + credentials { + val nexusUsername: String by project + val nexusPassword: String by project + + username = nexusUsername + password = nexusPassword + } + } + maven { + name = "snapshots" + // change to point to your repo, e.g. http://my.org/repo + url = uri("http://nexus.astraeus.nl/nexus/content/repositories/snapshots") + credentials { + val nexusUsername: String by project + val nexusPassword: String by project + + username = nexusUsername + password = nexusPassword + } + } + maven { + name = "sonatype" + setUrl("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") + credentials { + username = ossrhUsername + password = ossrhPassword + } + } + } + + // Configure all publications + publications.withType { + + // Stub javadoc.jar artifact + //artifact(javadocJar.get()) + + // Provide artifacts information requited by Maven Central + pom { + name.set("kotlin-css-generator") + description.set("Kotlin css generator") + url.set("https://github.com/rnentjes/kotlin-css-generator") + + licenses { + license { + name.set("MIT") + url.set("https://opensource.org/licenses/MIT") + } + } + developers { + developer { + id.set("rnentjes") + name.set("Rien Nentjes") + email.set("info@nentjes.com") + } + } + scm { + url.set("https://github.com/rnentjes/kotlin-css-generator") + } + } + } + +} + +signing { + sign(publishing.publications) +} diff --git a/gradle.properties.example b/gradle.properties.example new file mode 100644 index 0000000..a669e15 --- /dev/null +++ b/gradle.properties.example @@ -0,0 +1,11 @@ +kotlin.code.style=official +kotlin.js.compiler=both + +nexusUsername=deployment +nexusPassword= + +signingKeyId= +signingPassword= +signingSecretKeyRingFile= +ossrhUsername= +ossrhPassword= \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f3d88b1 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ffed3a2 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..2fe81a7 --- /dev/null +++ b/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9618d8d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/publish/build.gradle.kts b/publish/build.gradle.kts new file mode 100644 index 0000000..e2c0afa --- /dev/null +++ b/publish/build.gradle.kts @@ -0,0 +1,98 @@ +import java.util.* + +plugins { + `maven-publish` + signing +} + +repositories { + mavenCentral() +} + +project.extra.set("PUBLISH_GROUP_ID", "nl.astraeus") +project.extra.set("PUBLISH_VERSION", "1.0.0") +project.extra.set("PUBLISH_ARTIFACT_ID", "kotlin-css-generator") + +apply(from = "${rootProject.projectDir}/build.gradle.kts") + +// Stub secrets to let the project sync and build without the publication values set up +extra["signing.keyId"] = null +extra["signing.password"] = null +extra["signing.secretKeyRingFile"] = null +extra["ossrhUsername"] = null +extra["ossrhPassword"] = null + +// Grabbing secrets from local.properties file or from environment variables, which could be used on CI +val secretPropsFile = project.rootProject.file("local.properties") +if (secretPropsFile.exists()) { + secretPropsFile.reader().use { + Properties().apply { + load(it) + } + }.onEach { (name, value) -> + extra[name.toString()] = value + } +} else { + extra["signing.keyId"] = System.getenv("SIGNING_KEY_ID") + extra["signing.password"] = System.getenv("SIGNING_PASSWORD") + extra["signing.secretKeyRingFile"] = System.getenv("SIGNING_SECRET_KEY_RING_FILE") + extra["ossrhUsername"] = System.getenv("OSSRH_USERNAME") + extra["ossrhPassword"] = System.getenv("OSSRH_PASSWORD") +} + +val javadocJar by tasks.registering(Jar::class) { + archiveClassifier.set("javadoc") +} + +fun getExtraString(name: String) = extra[name]?.toString() + +publishing { + // Configure maven central repository + repositories { + maven { + name = "sonatype" + setUrl("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") + credentials { + username = getExtraString("ossrhUsername") + password = getExtraString("ossrhPassword") + } + } + } + + // Configure all publications + publications.withType { + + // Stub javadoc.jar artifact + artifact(javadocJar.get()) + + // Provide artifacts information requited by Maven Central + pom { + name.set("kotlin-css-generator") + description.set("Kotlin css generator") + url.set("https://github.com/rnentjes/kotlin-css-generator") + + licenses { + license { + name.set("MIT") + url.set("https://opensource.org/licenses/MIT") + } + } + developers { + developer { + id.set("rnentjes") + name.set("Rien Nentjes") + email.set("info@nentjes.com") + } + } + scm { + url.set("https://github.com/rnentjes/kotlin-css-generator") + } + } + } +} + +// Signing artifacts. Signing.* extra properties values will be used + +signing { + sign(publishing.publications) +} diff --git a/publish/publish-mavencentral.gradle b/publish/publish-mavencentral.gradle new file mode 100644 index 0000000..7b6cc5b --- /dev/null +++ b/publish/publish-mavencentral.gradle @@ -0,0 +1,121 @@ + +apply plugin: 'maven-publish' +apply plugin: 'signing' + +task publishSourcesJar(type: Jar) { + archiveClassifier.set('sources') + + // For pure Kotlin libraries, in case you have them + from sourceSets.main.java.srcDirs + //from sourceSets.main.kotlin.srcDirs +} + +task packageJavadoc(type: Jar) { + from javadoc + classifier = 'javadoc' +} + +artifacts { + archives publishSourcesJar +} + +File secretPropsFile = project.rootProject.file('local.properties') + +ext["signing.keyId"] = '' +ext["signing.password"] = '' +ext["signing.secretKeyRingFile"] = '' +ext["ossrhUsername"] = '' +ext["ossrhPassword"] = '' +ext["sonatypeStagingProfileId"] = '' + +if (secretPropsFile.exists()) { + Properties p = new Properties() + + new FileInputStream(secretPropsFile).withCloseable { is -> + p.load(is) + } + + p.each { name, value -> + ext[name] = value + } +} + +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + // The coordinates of the library, being set from variables that + // we'll set up later + groupId PUBLISH_GROUP_ID + artifactId PUBLISH_ARTIFACT_ID + version PUBLISH_VERSION + + // Two artifacts, the `aar` (or `jar`) and the sources + if (project.plugins.findPlugin("com.android.library")) { + artifact("$buildDir/outputs/aar/${project.getName()}-release.aar") + } else { + artifact("$buildDir/libs/${project.getName()}-${version}.jar") + } + artifact publishSourcesJar + artifact packageJavadoc + + // Mostly self-explanatory metadata + pom { + name = PUBLISH_ARTIFACT_ID + description = 'Simple JDBC wrapper for query statistics' + url = 'https://github.com/rnentjes/Simple-jdbc-statistics' + licenses { + license { + name = 'MIT License' + url = 'https://github.com/rnentjes/Simple-jdbc-statistics/blob/master/LICENCE.txt' + } + } + developers { + developer { + id = 'rnentjes' + name = 'Rien Nentjes' + email = 'info@nentjes.com' + } + // Add all other devs here... + } + // Version control info - if you're using GitHub, follow the format as seen here + scm { + connection = 'scm:git:github.com/rnentjes/Simple-jdbc-statistics.git' + developerConnection = 'scm:git:ssh://github.com/rnentjes/Simple-jdbc-statistics.git' + url = 'https://github.com/rnentjes/Simple-jdbc-statistics.git/tree/main' + } + // A slightly hacky fix so that your POM will include any transitive dependencies + // that your library builds upon + /* + witXml { + def dependenciesNode = asNode().appendNode('dependencies') + + project.configurations.implementation.allDependencies.each { + def dependencyNode = dependenciesNode.appendNode('dependency') + dependencyNode.appendNode('groupId', it.group) + dependencyNode.appendNode('artifactId', it.name) + dependencyNode.appendNode('version', it.version) + } + }*/ + } + } + } + // The repository to publish to, Sonatype/MavenCentral + repositories { + maven { + // This is an arbitrary name, you may also use "mavencentral" or + // any other name that's descriptive for you + name = "sonatype" + url = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" + credentials { + username ossrhUsername + password ossrhPassword + } + } + } + } +} + +signing { + sign publishing.publications +} diff --git a/publish/settings.gradle.kts b/publish/settings.gradle.kts new file mode 100644 index 0000000..fffc2a6 --- /dev/null +++ b/publish/settings.gradle.kts @@ -0,0 +1,12 @@ +pluginManagement { + repositories { + mavenCentral() + + maven { setUrl("https://plugins.gradle.org/m2/") } + } +} + +rootProject.name = "publish-kotlin-css-generator" + +//enableFeaturePreview("GRADLE_METADATA") +//include(":publish") \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..b32e1b5 --- /dev/null +++ b/readme.md @@ -0,0 +1,160 @@ +# Css generator like less/sass in kotlin multiplatform + +This library is for generating css from a kotlin dsl. +It can be used as an alternative to less/sass or as a runtime library to generate css on-the-fly. + +Tools like less and sass are often used as a build step and take some time. +This library is meant to be fast enough to generate the css on the fly either from the server or directly in the browser. + +Examples: + +## Nesting / colors / variables + +```kotlin + val color = hsla(0, 50, 50, 1.0) + val backgroundColor = Color.white + + val css = style { + select(cls("button")) { + padding(5.px) + + select("a") { + color(color) + backgroundColor(backgroundColor) + + hover { + color(color.lighten(10)) + backgroundColor(backgroundColor.darken(10)) + } + } + } + } +``` + +To generate the css call get generateCss function: + +```kotlin + val cssString: String = css.generateCss() +``` + +Result: + +```css +.button { + padding: 5px; +} + +.button a { + color: hsla(0, 50%, 50%, 1.0); + background-color: white; +} + +.button a:hover { + color: hsla(0, 50%, 55%, 1.0); + background-color: rgba(229, 229, 229, 1.0); +} +``` + +There are several options when generating the css, for example minified: + + +```kotlin + val cssString: String = css.generateCss(minified = true) +``` + +Result: + +```css +.button{padding:5px;}.buttona{color:hsla(0,50%,50%,1.0);background-color:white;}.buttona:hover{color:hsla(0,50%,55%,1.0);background-color:rgba(229,229,229,1.0);} +``` + +## Mixins + +As it's all just kotlin code, includes and mixins etc. are just functions calls. + +```kotlin + fun Style.borderStyles(borderWidth: Measurement = 2.px) { + borderWidth(borderWidth) + borderColor(Color.aquamarine) + borderStyle(BorderStyle.solid) + } + + val css = style { + select(txt("a"), cls("button")) { + borderStyles() + + color(Color.white) + } + + select(cls("btn-primary")) { + borderStyles(3.px) + color(Color.blue) + } + } +``` + +Result: + +```css +a { + border-width: 2px; + border-color: aquamarine; + border-style: solid; + color: white; +} + +.button { + border-width: 2px; + border-color: aquamarine; + border-style: solid; + color: white; +} + +.btn-primary { + border-width: 3px; + border-color: aquamarine; + border-style: solid; + color: blue; +} +``` + +Giving the option combineEqualBlocks to the generateCss call will combine the a and .button blocks with the following result: + +```css +a, +.button { + border-width: 2px; + border-color: aquamarine; + border-style: solid; + color: white; +} + +.btn-primary { + border-width: 3px; + border-color: aquamarine; + border-style: solid; + color: blue; +} +``` + +## Measurements + +Sizes and widths are given in measurements, there are extension variables to help with these: + +```kotlin + select("body") { + fontSize(1.2.em) + borderWidth(3.px) + width(75.prc) + } +``` + +Result: + +```css +body { + font-size: 1.2em; + border-width: 3px; + width: 75%; +} +``` diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..a22324a --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,13 @@ +pluginManagement { + repositories { + maven { setUrl("https://dl.bintray.com/kotlin/kotlin-eap") } + + mavenCentral() + + maven { setUrl("https://plugins.gradle.org/m2/") } + } +} + +rootProject.name = "kotlin-css-generator" + +//enableFeaturePreview("GRADLE_METADATA") diff --git a/src/commonMain/kotlin/nl/astraeus/css/CssBuilder.kt b/src/commonMain/kotlin/nl/astraeus/css/CssBuilder.kt new file mode 100644 index 0000000..c2e0422 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/CssBuilder.kt @@ -0,0 +1,30 @@ +package nl.astraeus.css + +import nl.astraeus.css.style.ConditionalCss +import nl.astraeus.css.style.ConditionalStyle +import nl.astraeus.css.style.Css +import nl.astraeus.css.style.Style + +fun css(definition: Css) = definition + +fun style(definition: ConditionalCss): ConditionalStyle { + val css = ConditionalStyle() + + definition(css) + + return css +} + +class CssBuilder { + var definition: Style = Style() + + fun style(definition: Style.() -> Unit) { + definition(this.definition) + } + + fun getCss(minified: Boolean = false): String = definition.generateCss(minified = minified) + + override fun toString(): String { + return "CssBuilder(${definition.generateCss()})" + } +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/AlignContent.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/AlignContent.kt new file mode 100644 index 0000000..deeeb8f --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/AlignContent.kt @@ -0,0 +1,17 @@ +package nl.astraeus.css.properties + +class AlignContent( + value: String +) : CssProperty(value) { + + companion object { + val stretch = AlignContent("stretch") + val center = AlignContent("center") + val flexStart = AlignContent("flex-start") + val flexEnd = AlignContent("flex-end") + val spaceBetween = AlignContent("space-between") + val spaceAround = AlignContent("space-around") + val initial = AlignContent("initial") + val inherit = AlignContent("inherit") + } +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/AlignItems.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/AlignItems.kt new file mode 100644 index 0000000..99c8da1 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/AlignItems.kt @@ -0,0 +1,17 @@ +package nl.astraeus.css.properties + +class AlignItems( + value: String +) : CssProperty(value) { + + companion object { + val stretch = AlignItems("stretch") + val center = AlignItems("center") + val flexStart = AlignItems("flex-start") + val flexEnd = AlignItems("flex-end") + val baseline = AlignItems("baseline") + val initial = AlignItems("initial") + val inherit = AlignItems("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/AlignSelf.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/AlignSelf.kt new file mode 100644 index 0000000..b721a88 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/AlignSelf.kt @@ -0,0 +1,18 @@ +package nl.astraeus.css.properties + +class AlignSelf( + value: String +) : CssProperty(value) { + + companion object { + val auto = AlignSelf("auto") + val stretch = AlignSelf("stretch") + val center = AlignSelf("center") + val flexStart = AlignSelf("flex-start") + val flexEnd = AlignSelf("flex-end") + val baseline = AlignSelf("baseline") + val initial = AlignSelf("initial") + val inherit = AlignSelf("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/All.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/All.kt new file mode 100644 index 0000000..d9bcb41 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/All.kt @@ -0,0 +1,14 @@ +package nl.astraeus.css.properties + +class All( + value: String +) : CssProperty(value) { + + companion object { + val unset = All("unset") + val revert = All("revert") + val initial = All("initial") + val inherit = All("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Animation.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Animation.kt new file mode 100644 index 0000000..6483c89 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Animation.kt @@ -0,0 +1,55 @@ +package nl.astraeus.css.properties + +class AnimationDirection( + value: String +) : CssProperty(value) { + + companion object { + val normal = AnimationDirection("normal") + val reverse = AnimationDirection("reverse") + val alternate = AnimationDirection("alternate") + val alternateReverse = AnimationDirection("alternate-reverse") + val initial = AnimationDirection("initial") + val inherit = AnimationDirection("inherit") + } +} + +class AnimationFillMode( + value: String +) : CssProperty(value) { + + companion object { + val none = AnimationFillMode("none") + val forwards = AnimationFillMode("forwards") + val backwards = AnimationFillMode("backwards") + val both = AnimationFillMode("both") + val initial = AnimationFillMode("initial") + val inherit = AnimationFillMode("inherit") + } +} + +class AnimationFrame( + value: String = "" +) : CssProperty(value) { + + companion object { + fun name(name: String) = AnimationFrame(name) + val none: AnimationFrame = AnimationFrame("none") + val initial: AnimationFrame = AnimationFrame("initial") + val inherit: AnimationFrame = AnimationFrame("inherit") + } +} + + +class AnimationPlayState( + value: String +) : CssProperty(value) { + + companion object { + fun name(name: String) = AnimationPlayState(name) + val paused = AnimationPlayState("paused") + val running = AnimationPlayState("running") + val initial = AnimationPlayState("initial") + val inherit = AnimationPlayState("inherit") + } +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/BackfaceVisibility.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/BackfaceVisibility.kt new file mode 100644 index 0000000..698379a --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/BackfaceVisibility.kt @@ -0,0 +1,13 @@ +package nl.astraeus.css.properties + +class BackfaceVisibility( + value: String +) : CssProperty(value) { + + companion object { + val visible = BackfaceVisibility("visible") + val hidden = BackfaceVisibility("hidden") + val initial = BackfaceVisibility("initial") + val inherit = BackfaceVisibility("inherit") + } +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Background.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Background.kt new file mode 100644 index 0000000..46554b4 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Background.kt @@ -0,0 +1,79 @@ +package nl.astraeus.css.properties + +class BackgroundAttachment( + value: String +) : CssProperty(value) { + + companion object { + val scroll = BackgroundAttachment("scroll") + val fixed = BackgroundAttachment("fixed") + val local = BackgroundAttachment("local") + val initial = BackgroundAttachment("initial") + val inherit = BackgroundAttachment("inherit") + } +} + +class BackgroundBlendMode( + value: String +) : CssProperty(value) { + + companion object { + val normal = BackgroundBlendMode("normal") + val multiply = BackgroundBlendMode("multiply") + val screen = BackgroundBlendMode("screen") + val overlay = BackgroundBlendMode("overlay") + val darken = BackgroundBlendMode("darken") + val lighten = BackgroundBlendMode("lighten") + val colorDodge = BackgroundBlendMode("color-dodge") + val saturation = BackgroundBlendMode("saturation") + val color = BackgroundBlendMode("color") + val luminosity = BackgroundBlendMode("luminosity") + } +} + +class BackgroundPosition( + value: String +) : CssProperty(value) { + + companion object { + val left = BackgroundPosition("left") + val center = BackgroundPosition("center") + val right = BackgroundPosition("right") + val initial = BackgroundPosition("initial") + val inherit = BackgroundPosition("inherit") + } +} + +class BackgroundRepeat( + value: String +) : CssProperty(value) { + + companion object { + val repeat = BackgroundRepeat("repeat") + val repeatX = BackgroundRepeat("repeat-x") + val repeatY = BackgroundRepeat("repeat-y") + val noRepeat = BackgroundRepeat("no-repeat") + val space = BackgroundRepeat("space") + val round = BackgroundRepeat("round") + val initial = BackgroundRepeat("initial") + val inherit = BackgroundRepeat("inherit") + val unset = BackgroundRepeat("unset") + } +} + +class BackgroundSize( + value: String +) : CssProperty(value) { + + companion object { + fun px(px: Int) = BackgroundSize("${px}px") + fun perc(pc: Double) = BackgroundSize("${pc}%") + + val auto = BackgroundSize("auto") + val cover = BackgroundSize("cover") + val contain = BackgroundSize("contain") + val initial = BackgroundSize("initial") + val inherit = BackgroundSize("inherit") + } +} + diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Border.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Border.kt new file mode 100644 index 0000000..ea4466f --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Border.kt @@ -0,0 +1,78 @@ +package nl.astraeus.css.properties + +class BorderStyle( + value: String +) : CssProperty(value) { + + companion object { + val none = BorderStyle("none") + val hidden = BorderStyle("hidden") + val dotted = BorderStyle("dotted") + val dashed = BorderStyle("dashed") + val solid = BorderStyle("solid") + val double = BorderStyle("double") + val groove = BorderStyle("groove") + val ridge = BorderStyle("ridge") + val inset = BorderStyle("inset") + val outset = BorderStyle("outset") + val initial = BorderStyle("initial") + val inherit = BorderStyle("inherit") + } +} + +class BorderWidth( + value: String +) : CssProperty(value) { + + companion object { + val thin = BorderWidth("thin") + val medium = BorderWidth("medium") + val thick = BorderWidth("thick") + val initial = BorderWidth("initial") + val inherit = BorderWidth("inherit") + } +} + +class BorderCollapse( + value: String +) : CssProperty(value) { + + companion object { + val separate = BorderCollapse("separate") + val collapse = BorderCollapse("collapse") + } +} + +class BorderImageWidth( + value: String +) : CssProperty(value) { + + companion object { + fun px(nr: Int) = BorderImageWidth("${nr}px") + fun nr(nr: Int) = BorderImageWidth("$nr") + fun perc(nr: Int) = BorderImageWidth("${nr}%") + fun perc(nr: Double) = BorderImageWidth("${nr}%") + val auto = BorderImageWidth("auto") + val initial = BorderImageWidth("initial") + val inherit = BorderImageWidth("inherit") + } +} + +class BorderSpacing( + value: String +) : CssProperty(value) { + + companion object { + fun px(nr: Int) = BorderSpacing("${nr}px") + fun em(nr: Int) = BorderSpacing("${nr}em") + fun em(nr: Double) = BorderSpacing("${nr}em") + fun perc(nr: Int) = BorderSpacing("${nr}%") + fun perc(nr: Double) = BorderSpacing("${nr}%") + fun pc(nr: Int) = BorderSpacing("${nr}pc") + fun pc(nr: Double) = BorderSpacing("${nr}pc") + fun cm(nr: Int) = BorderSpacing("${nr}cm") + fun cm(nr: Double) = BorderSpacing("${nr}cm") + val initial = BorderSpacing("initial") + val inherit = BorderSpacing("inherit") + } +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Box.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Box.kt new file mode 100644 index 0000000..930c10c --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Box.kt @@ -0,0 +1,40 @@ +package nl.astraeus.css.properties + +class BoxDecorationBreak( + value: String +) : CssProperty(value) { + + companion object { + val slice = BoxDecorationBreak("slice") + val clone = BoxDecorationBreak("clone") + val initial = BoxDecorationBreak("initial") + val inherit = BoxDecorationBreak("inherit") + val unset = BoxDecorationBreak("unset") + } +} + +class BoxShadow( + value: String +) : CssProperty(value) { + + companion object { + val none = BoxShadow("none") + val inset = BoxShadow("inset") + val initial = BoxShadow("initial") + val inherit = BoxShadow("inherit") + + fun text(txt: String) = BoxShadow(txt) + } +} + +class BoxSizing( + value: String +) : CssProperty(value) { + + companion object { + val contextBox = BoxSizing("content-box") + val borderBox = BoxSizing("border-box") + val initial = BoxShadow("initial") + val inherit = BoxShadow("inherit") + } +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Break.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Break.kt new file mode 100644 index 0000000..017b0c5 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Break.kt @@ -0,0 +1,27 @@ +package nl.astraeus.css.properties + + +class Break( + value: String +) : CssProperty(value) { + + companion object { + val auto = Break("auto") + val all = Break("all") + val always = Break("always") + val avoid = Break("avoid") + val avoidColumn = Break("avoid-column") + val avoidPage = Break("avoid-page") + val avoidRegion = Break("avoid-region") + val column = Break("column") + val left = Break("left") + val page = Break("page") + val recto = Break("recto") + val region = Break("region") + val right = Break("right") + val verso = Break("verso") + val initial = Break("initial") + val inherit = Break("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Caption.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Caption.kt new file mode 100644 index 0000000..a595b8b --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Caption.kt @@ -0,0 +1,14 @@ +package nl.astraeus.css.properties + +class CaptionSide( + value: String +) : CssProperty(value) { + + companion object { + val top = CaptionSide("top") + val bottom = CaptionSide("bottom") + val initial = CaptionSide("initial") + val inherit = CaptionSide("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Clear.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Clear.kt new file mode 100644 index 0000000..8c52912 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Clear.kt @@ -0,0 +1,17 @@ +package nl.astraeus.css.properties + + +class Clear( + value: String +) : CssProperty(value) { + + companion object { + val none = Clear("none") + val left = Clear("left") + val right = Clear("right") + val both = Clear("both") + val initial = Clear("initial") + val inherit = Clear("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Clip.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Clip.kt new file mode 100644 index 0000000..da885a2 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Clip.kt @@ -0,0 +1,57 @@ +package nl.astraeus.css.properties + +class Clip( + value: String +) : CssProperty(value) { + + companion object { + fun rect(top: Int, right: Int, bottom: Int, left: Int) = Clip("rect(${top}px,${right}px,${bottom}px,${left}px)") + val auto = Clip("auto") + val initial = Clip("initial") + val inherit = Clip("inherit") + + } +} + +class ClipPath( + value: String +) : CssProperty(value) { + + companion object { + val auto = ClipPath("auto") + fun circle(perc: Double) = ClipPath("circle(${perc}%)") + fun ellipse(radiusX: Double, radiusY: Double) = ClipPath("ellipse(${radiusX}%,${radiusY}%)") + fun ellipse( + radiusX: Double, + radiusY: Double, + positionX: Double, + positionY: Double + ) = ClipPath("ellipse(${radiusX}%,${radiusY}% at ${positionX}%,${positionY}%)") + + // todo: other options + fun other(text: String) = ClipPath(text) + + val marginBox = ClipPath("margin-box") + val borderBox = ClipPath("border-box") + val paddingBox = ClipPath("padding-box") + val contentBox = ClipPath("content-box") + val fillBox = ClipPath("fill-box") + val strokeBox = ClipPath("stroke-box") + val viewBox = ClipPath("view-box") + val none = ClipPath("none") + } +} + +class ClipOrigin( + value: String +) : CssProperty(value) { + + companion object { + val borderBox = ClipOrigin("border-box") + val paddingBox = ClipOrigin("padding-box") + val contentBox = ClipOrigin("content-box") + val initial = ClipOrigin("initial") + val inherit = ClipOrigin("inherit") + + } +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Color.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Color.kt new file mode 100644 index 0000000..adc6bdb --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Color.kt @@ -0,0 +1,594 @@ +package nl.astraeus.css.properties + +import kotlin.math.PI +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.roundToInt + +private val hexString = "0123456789abcdef" + +private fun Int.toColorHex(minimumDigits: Int = 2): String { + val result = StringBuilder() + var value = this + + while(value > 0) { + result.append(hexString[value%16]) + + value /= 16 + } + + while(result.length < minimumDigits) { + result.append("0") + } + + return result.reverse().toString() +} + +/** + * See [CSS Color Module Level 3](https://www.w3.org/TR/2018/REC-css-color-3-20180619/) + * + * This class represents a CSS color value. String parameters to the constructor argument + * can take one of the following forms: + * + * * HTML color name, e.g. ``Red``, ``DarkSalmon`` (case-insensitive), though in this case the use of the pre-defined constants is recommended. + * * ``#rgb`` or ``#rrggbb`` + * * ``rgb(0..255, 0..255, 0..255)``, ``rgb(0..100%, 0..100%, 0..100%)``, ``rgb(0..100%, 0..100%, 0..100%, 0..1)``, ``rgba(0..255, 0..255, 0..255, 0..1)`` + * * ``hsl(0..360, 0-100%, 0..100%)`` or ``hsla(0..360, 0-100%, 0..100%, 0..1)`` + * + * Technically, the Hue parameter to ``hsl`` or ``hsla`` can exceed ``360``, because it represents a *degree* (angle) on + * the color wheel. But as per the algorithm proposed by the W3C, the value will ultimately be capped to ``360`` through + * a series of modulus operations; see section *4.2.4. HSL color values* of the above specification. + * + * Taken from: https://github.com/JetBrains/kotlin-wrappers/tree/master/kotlin-css + */ +@Suppress("SpellCheckingInspection") +class Color(value: String) : CssProperty(value) { + private var rgb: String? = null + + private constructor(value: String, rgb: String) : this(value) { + this.rgb = rgb + } + + fun hasAlpha(): Boolean = isRgba() || isHexa() || isHsla() + + fun getAlpha(): Double = when { + isHexa() || isRgba() -> { + toRGBA().alpha + } + isHsla() -> { + fromHSLANotation().alpha + } + else -> { + 1.0 + } + } + + fun toHex(): String = if (isHsla() || isHsl()) { + fromHSLANotation().asRGBA().asHex() + } else { + toRGBA().asHex() + } + + fun isHsla(): Boolean { + val v = rgb ?: value + + return v.startsWith("hsla") + } + + fun isHsl(): Boolean { + val v = rgb ?: value + + return v.startsWith("hsl(") + } + + fun isRgba(): Boolean { + val v = rgb ?: value + + return v.startsWith("rgba(") + } + + fun isRgb(): Boolean { + val v = rgb ?: value + + return v.startsWith("rgb(") + } + + fun isHex(): Boolean { + val v = rgb ?: value + + return v.startsWith("#") && v.length < 8 + } + + fun isHexa(): Boolean { + val v = rgb ?: value + + return v.startsWith("#") && v.length > 7 + } + + /** + * withAlpha preserves existing alpha value: rgba(0, 0, 0, 0.5).withAlpha(0.1) = rgba(0, 0, 0, 0.05) + */ + fun withAlpha(alpha: Double) = + when { + value.startsWith("hsl", true) -> with(fromHSLANotation()) { hsla(hue, saturation, lightness, normalizeAlpha(alpha) * this.alpha) } + else -> with(toRGBA()) { rgba(red, green, blue, normalizeAlpha(alpha) * this.alpha) } + } + + /** + * changeAlpha rewrites existing alpha value: rgba(0, 0, 0, 0.5).withAlpha(0.1) = rgba(0, 0, 0, 0.1) + */ + fun changeAlpha(alpha: Double) = + when { + value.startsWith("hsl", true) -> with(fromHSLANotation()) { hsla(hue, saturation, lightness, normalizeAlpha(alpha)) } + else -> with(toRGBA()) { rgba(red, green, blue, normalizeAlpha(alpha)) } + } + + // https://stackoverflow.com/questions/2049230/convert-rgba-color-to-rgb + fun blend(backgroundColor: Color): Color { + val source = this.toRGBA() + val background = backgroundColor.toRGBA() + + val targetR = ((1 - source.alpha) * background.red) + (source.alpha * source.red) + val targetG = ((1 - source.alpha) * background.green) + (source.alpha * source.green) + val targetB = ((1 - source.alpha) * background.blue) + (source.alpha * source.blue) + + return rgb(targetR.roundToInt(), targetG.roundToInt(), targetB.roundToInt()) + } + + /** + * Lighten the color by the specified percent (between 0-100), returning a new instance of Color. + * + * @param percent the percent to lighten the Color + * @return a new lightened version of this color + */ + fun lighten(percent: Int): Color { + val isHSLA = value.startsWith("hsl", ignoreCase = true) + val hsla = if (isHSLA) fromHSLANotation() else toRGBA().asHSLA() + + val lightness = hsla.lightness + (hsla.lightness * (normalizePercent(percent) / 100.0)).roundToInt() + val newHSLa = hsla.copy(lightness = normalizePercent(lightness)) + return if (isHSLA) { + hsla(newHSLa.hue, newHSLa.saturation, newHSLa.lightness, newHSLa.alpha) + } else { + with(newHSLa.asRGBA()) { rgba(red, green, blue, alpha) } + } + } + + /** + * Darken the color by the specified percent (between 0-100), returning a new instance of Color. + * + * @param percent the percent to darken the Color + * @return a new darkened version of this color + */ + fun darken(percent: Int): Color { + val isHSLA = value.startsWith("hsl", ignoreCase = true) + val hsla = if (isHSLA) fromHSLANotation() else toRGBA().asHSLA() + + val darkness = hsla.lightness - (hsla.lightness * (normalizePercent(percent) / 100.0)).roundToInt() + val newHSLa = hsla.copy(lightness = normalizePercent(darkness)) + return if (isHSLA) { + hsla(newHSLa.hue, newHSLa.saturation, newHSLa.lightness, newHSLa.alpha) + } else { + with(newHSLa.asRGBA()) { rgba(red, green, blue, alpha) } + } + } + + /** + * Increase contrast, if lightness > 50 then darken else lighten + * + * @param percent the percent to lighten/darken the Color + * @return a new ligtened/darkened version of this color + */ + fun contrast(percent: Int): Color { + val isHSLA = value.startsWith("hsl", ignoreCase = true) + val hsla = if (isHSLA) fromHSLANotation() else toRGBA().asHSLA() + + val darkness = if (hsla.lightness > 50) { + hsla.lightness - (hsla.lightness * (normalizePercent(percent) / 100.0)).roundToInt() + } else { + hsla.lightness + (hsla.lightness * (normalizePercent(percent) / 100.0)).roundToInt() + } + + val newHSLa = hsla.copy(lightness = normalizePercent(darkness)) + return if (isHSLA) { + hsla(newHSLa.hue, newHSLa.saturation, newHSLa.lightness, newHSLa.alpha) + } else { + with(newHSLa.asRGBA()) { rgba(red, green, blue, alpha) } + } + } + + /** + * Saturate the color by the specified percent (between 0-100), returning a new instance of Color. + * + * @param percent the percent to saturate the Color + * @return a new saturated version of this color + */ + fun saturate(percent: Int): Color { + val isHSLA = value.startsWith("hsl", ignoreCase = true) + val hsla = if (isHSLA) fromHSLANotation() else toRGBA().asHSLA() + + val saturation = hsla.saturation + (hsla.saturation * (normalizePercent(percent) / 100.0)).roundToInt() + val newHSLa = hsla.copy(saturation = normalizePercent(saturation)) + return if (isHSLA) { + hsla(newHSLa.hue, newHSLa.saturation, newHSLa.lightness, newHSLa.alpha) + } else { + with(newHSLa.asRGBA()) { rgba(red, green, blue, alpha) } + } + } + + /** + * Desaturate the color by the specified percent (between 0-100), returning a new instance of Color. + * + * @param percent the percent to desaturate the Color + * @return a new desaturated version of this color + */ + fun desaturate(percent: Int): Color { + val isHSLA = value.startsWith("hsl", ignoreCase = true) + val hsla = if (isHSLA) fromHSLANotation() else toRGBA().asHSLA() + + val desaturation = hsla.saturation - (hsla.saturation * (normalizePercent(percent) / 100.0)).roundToInt() + val newHSLa = hsla.copy(saturation = normalizePercent(desaturation)) + return if (isHSLA) { + hsla(newHSLa.hue, newHSLa.saturation, newHSLa.lightness, newHSLa.alpha) + } else { + with(newHSLa.asRGBA()) { rgba(red, green, blue, alpha) } + } + } + + internal data class RGBA( + val red: Int, + val green: Int, + val blue: Int, + val alpha: Double = 1.0 + ) { + + // Algorithm adapted from http://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/ + fun asHSLA(): HSLA { + // scale R, G, B values into 0..1 fractions + val r = red / 255.0 + val g = green / 255.0 + val b = blue / 255.0 + + val cMax = maxOf(r, g, b) + val cMin = minOf(r, g, b) + val chroma = cMax - cMin + + val lg = normalizeFractionalPercent((cMax + cMin) / 2) + val s = if (chroma != 0.0) normalizeFractionalPercent(chroma / (1.0 - abs((2.0 * lg) - 1.0))) else 0.0 + val h = when (cMax) { + cMin -> 0.0 + r -> 60 * (((g - b) / chroma) % 6.0) + g -> 60 * (((b - r) / chroma) + 2) + b -> 60 * (((r - g) / chroma) + 4) + else -> error("Unexpected value for max") // theoretically unreachable bc maxOf(r, g, b) above + } + + return HSLA(normalizeHue(h), (s * 100).roundToInt(), (lg * 100).roundToInt(), alpha) + } + + fun asHex(): String { + val result = StringBuilder() + + result.append(red.toColorHex(2)) + result.append(green.toColorHex(2)) + result.append(blue.toColorHex(2)) + + return result.toString() + } + } + + internal data class HSLA( + val hue: Int, + val saturation: Int, + val lightness: Int, + val alpha: Double = 1.0 + ) { + + // Algorithm from W3C link referenced in class comment (section 4.2.4. HSL color values) + fun asRGBA(): RGBA { + fun hueToRGB(m1: Double, m2: Double, h: Double): Double { + val hu = if (h < 0) h + 1 else if (h > 1) h - 1 else h + return when { + (hu < 1.0 / 6) -> m1 + (m2 - m1) * 6 * hu + (hu < 1.0 / 2) -> m2 + (hu < 2.0 / 3) -> m1 + ((m2 - m1) * 6 * (2.0 / 3 - hu)) + else -> m1 + } + } + + val lightness255 = lightness * 255 / 100 + if (saturation == 0) return RGBA(lightness255, lightness255, lightness255) + + // scale H, S, V values into 0..1 fractions + val h = (hue % 360.0) / 360.0 + val s = saturation / 100.0 + val lg = lightness / 100.0 + + val m2 = if (lg < 0.5) lg * (1 + s) else (lg + s - lg * s) + val m1 = 2 * lg - m2 + val r = normalizeFractionalPercent(hueToRGB(m1, m2, h + (1.0 / 3))) + val g = normalizeFractionalPercent(hueToRGB(m1, m2, h)) + val b = normalizeFractionalPercent(hueToRGB(m1, m2, h - (1.0 / 3))) + return RGBA((r * 255).roundToInt(), (g * 255).roundToInt(), (b * 255).roundToInt(), alpha) + } + } + + internal fun fromHSLANotation(): HSLA { + val match = HSLA_REGEX.find(value) + + fun getHSLParameter(index: Int) = + match?.groups?.get(index)?.value + ?: throw IllegalArgumentException("Expected hsl or hsla notation, got $value") + + val hueShape = getHSLParameter(1) + val hue = normalizeHue( + when { + hueShape.endsWith("grad", true) -> hueShape.substringBefore("grad").toDouble() * (9.0 / 10) + hueShape.endsWith("rad", true) -> (hueShape.substringBefore("rad").toDouble() * 180) / PI + hueShape.endsWith("turn", true) -> hueShape.substringBefore("turn").toDouble() * 360.0 + hueShape.endsWith("deg", true) -> hueShape.substringBefore("deg").toDouble() + else -> hueShape.toDouble() + } + ) + val saturation = normalizePercent(getHSLParameter(2).toInt()) + val lightness = normalizePercent(getHSLParameter(3).toInt()) + val alpha = normalizeAlpha(match?.groups?.get(4)?.value?.toDouble() ?: 1.0) + + return HSLA(hue, saturation, lightness, alpha) + } + + internal fun fromRGBANotation(): RGBA { + val match = RGBA_REGEX.find(value) + + fun getRGBParameter(index: Int): Int { + val group = match?.groups?.get(index)?.value + ?: throw IllegalArgumentException("Expected rgb or rgba notation, got $value") + + return when { + (group.endsWith('%')) -> (normalizeFractionalPercent(group.substringBefore('%').toDouble() / 100.0) * 255.0).toInt() + else -> normalizeRGB(group.toInt()) + } + } + + val red = getRGBParameter(1) + val green = getRGBParameter(2) + val blue = getRGBParameter(3) + val alpha = normalizeAlpha(match?.groups?.get(4)?.value?.toDouble() ?: 1.0) + + return RGBA(red, green, blue, alpha) + } + + internal fun toRGBA(): RGBA { + val v = rgb ?: value + return when { + v.startsWith("rgb") -> fromRGBANotation() + + // Matches #rgb + v.startsWith("#") && v.length == 4 -> RGBA( + "${v[1]}${v[1]}".toInt(16), + "${v[2]}${v[2]}".toInt(16), + "${v[3]}${v[3]}".toInt(16) + ) + + // Matches both #rrggbb + v.startsWith("#") && v.length == 7 -> RGBA( + (v.substring(1..2)).toInt(16), + (v.substring(3..4)).toInt(16), + (v.substring(5..6)).toInt(16) + ) + + // Matches both #rrggbbaa + v.startsWith("#") && v.length == 9 -> RGBA( + (v.substring(1..2)).toInt(16), + (v.substring(3..4)).toInt(16), + (v.substring(5..6)).toInt(16), + (v.substring(7..8)).toInt(16) / 255.0 + ) + else -> throw IllegalArgumentException("Only hexadecimal, rgb, and rgba notations are accepted, got $v") + } + } + + companion object { + val initial = Color("initial") + val inherit = Color("inherit") + val unset = Color("unset") + + val transparent = Color("transparent") + val currentColor = Color("currentColor") + + // W3C predefined HTML colors (147), see the referenced specification above. + val aliceBlue = Color("aliceblue", "#f0f8ff") + val antiqueWhite = Color("antiquewhite", "#faebd7") + val aqua = Color("aqua", "#00ffff") + val aquamarine = Color("aquamarine", "#7fffd4") + val azure = Color("azure", "#f0ffff") + val beige = Color("beige", "#f5f5dc") + val bisque = Color("bisque", "#ffe4c4") + val black = Color("black", "#000000") + val blanchedAlmond = Color("blanchedalmond", "#ffebcd") + val blue = Color("blue", "#0000ff") + val blueViolet = Color("blueviolet", "#8a2be2") + val brown = Color("brown", "#a52a2a") + val burlyWood = Color("burlywood", "#deb887") + val cadetBlue = Color("cadetblue", "#5f9ea0") + val chartreuse = Color("chartreuse", "#7fff00") + val chocolate = Color("chocolate", "#d2691e") + val coral = Color("coral", "#ff7f50") + val cornflowerBlue = Color("cornflowerblue", "#6495ed") + val cornsilk = Color("cornsilk", "#fff8dc") + val crimson = Color("crimson", "#dc143c") + val cyan = Color("cyan", "#00ffff") + val darkBlue = Color("darkblue", "#00008b") + val darkCyan = Color("darkcyan", "#008b8b") + val darkGoldenrod = Color("darkgoldenrod", "#b8860b") + val darkGray = Color("darkgray", "#a9a9a9") + val darkGreen = Color("darkgreen", "#006400") + val darkGrey = Color("darkgrey", "#a9a9a9") + val darkKhaki = Color("darkkhaki", "#bdb76b") + val darkMagenta = Color("darkmagenta", "#8b008b") + val darkOliveGreen = Color("darkolivegreen", "#556b2f") + val darkOrange = Color("darkorange", "#ff8c00") + val darkOrchid = Color("darkorchid", "#9932cc") + val darkRed = Color("darkred", "#8b0000") + val darkSalmon = Color("darksalmon", "#e9967a") + val darkSeaGreen = Color("darkseagreen", "#8fbc8f") + val darkSlateBlue = Color("darkslateblue", "#483d8b") + val darkSlateGray = Color("darkslategray", "#2f4f4f") + val darkSlateGrey = Color("darkslategrey", "#2f4f4f") + val darkTurquoise = Color("darkturquoise", "#00ced1") + val darkViolet = Color("darkviolet", "#9400d3") + val deepPink = Color("deeppink", "#ff1493") + val deepSkyBlue = Color("deepskyblue", "#00bfff") + val dimGray = Color("dimgray", "#696969") + val dimGrey = Color("dimgrey", "#696969") + val dodgerBlue = Color("dodgerblue", "#1e90ff") + val firebrick = Color("firebrick", "#b22222") + val floralWhite = Color("floralwhite", "#fffaf0") + val forestGreen = Color("forestgreen", "#228b22") + val fuchsia = Color("fuchsia", "#ff00ff") + val gainsboro = Color("gainsboro", "#dcdcdc") + val ghostWhite = Color("ghostwhite", "#f8f8ff") + val gold = Color("gold", "#ffd700") + val goldenrod = Color("goldenrod", "#daa520") + val gray = Color("gray", "#808080") + val green = Color("green", "#008000") + val greenYellow = Color("greenyellow", "#adff2f") + val grey = Color("grey", "#808080") + val honeydew = Color("honeydew", "#f0fff0") + val hotPink = Color("hotpink", "#ff69b4") + val indianRed = Color("indianred", "#cd5c5c") + val indigo = Color("indigo", "#4b0082") + val ivory = Color("ivory", "#fffff0") + val khaki = Color("khaki", "#f0e68c") + val lavender = Color("lavender", "#e6e6fa") + val lavenderBlush = Color("lavenderblush", "#fff0f5") + val lawnGreen = Color("lawngreen", "#7cfc00") + val lemonChiffon = Color("lemonchiffon", "#fffacd") + val lightBlue = Color("lightblue", "#add8e6") + val lightCoral = Color("lightcoral", "#f08080") + val lightCyan = Color("lightcyan", "#e0ffff") + val lightGoldenrodYellow = Color("lightgoldenrodyellow", "#fafad2") + val lightGray = Color("lightgray", "#d3d3d3") + val lightGreen = Color("lightgreen", "#90ee90") + val lightGrey = Color("lightgrey", "#d3d3d3") + val lightPink = Color("lightpink", "#ffb6c1") + val lightSalmon = Color("lightsalmon", "#ffa07a") + val lightSeaGreen = Color("lightseagreen", "#20b2aa") + val lightSkyBlue = Color("lightskyblue", "#87cefa") + val lightSlateGray = Color("lightslategray", "#778899") + val lightSlateGrey = Color("lightslategrey", "#778899") + val lightSteelBlue = Color("lightsteelblue", "#b0c4de") + val lightYellow = Color("lightyellow", "#ffffe0") + val lime = Color("lime", "#00ff00") + val limeGreen = Color("limegreen", "#32cd32") + val linen = Color("linen", "#faf0e6") + val magenta = Color("magenta", "#ff00ff") + val maroon = Color("maroon", "#800000") + val mediumAquamarine = Color("mediumaquamarine", "#66cdaa") + val mediumBlue = Color("mediumblue", "#0000cd") + val mediumOrchid = Color("mediumorchid", "#ba55d3") + val mediumPurple = Color("mediumpurple", "#9370d8") + val mediumSeaGreen = Color("mediumseagreen", "#3cb371") + val mediumSlateBlue = Color("mediumslateblue", "#7b68ee") + val mediumSpringGreen = Color("mediumspringgreen", "#00fa9a") + val mediumTurquoise = Color("mediumturquoise", "#48d1cc") + val mediumVioletRed = Color("mediumvioletred", "#c71585") + val midnightBlue = Color("midnightblue", "#191970") + val mintCream = Color("mintcream", "#f5fffa") + val mistyRose = Color("mistyrose", "#ffe4e1") + val moccasin = Color("moccasin", "#ffe4b5") + val navajoWhite = Color("navajowhite", "#ffdead") + val navy = Color("navy", "#000080") + val oldLace = Color("oldlace", "#fdf5e6") + val olive = Color("olive", "#808000") + val oliveDrab = Color("olivedrab", "#6b8e23") + val orange = Color("orange", "#ffa500") + val orangeRed = Color("orangered", "#ff4500") + val orchid = Color("orchid", "#da70d6") + val paleGoldenrod = Color("palegoldenrod", "#eee8aa") + val paleGreen = Color("palegreen", "#98fb98") + val paleTurquoise = Color("paleturquoise", "#afeeee") + val paleVioletRed = Color("palevioletred", "#db7093") + val papayaWhip = Color("papayawhip", "#ffefd5") + val peachPuff = Color("peachpuff", "#ffdab9") + val peru = Color("peru", "#cd853f") + val pink = Color("pink", "#ffc0cb") + val plum = Color("plum", "#dda0dd") + val powderBlue = Color("powderblue", "#b0e0e6") + val purple = Color("purple", "#800080") + val red = Color("red", "#ff0000") + val rosyBrown = Color("rosybrown", "#bc8f8f") + val royalBlue = Color("royalblue", "#4169e1") + val saddleBrown = Color("saddlebrown", "#8b4513") + val salmon = Color("salmon", "#fa8072") + val sandyBrown = Color("sandybrown", "#f4a460") + val seaGreen = Color("seagreen", "#2e8b57") + val seaShell = Color("seashell", "#fff5ee") + val sienna = Color("sienna", "#a0522d") + val silver = Color("silver", "#c0c0c0") + val skyBlue = Color("skyblue", "#87ceeb") + val slateBlue = Color("slateblue", "#6a5acd") + val slateGray = Color("slategray", "#708090") + val slateGrey = Color("slategrey", "#708090") + val snow = Color("snow", "#fffafa") + val springGreen = Color("springgreen", "#00ff7f") + val steelBlue = Color("steelblue", "#4682b4") + val tan = Color("tan", "#d2b48c") + val teal = Color("teal", "#008080") + val thistle = Color("thistle", "#d8bfd8") + val tomato = Color("tomato", "#ff6347") + val turquoise = Color("turquoise", "#40e0d0") + val violet = Color("violet", "#ee82ee") + val wheat = Color("wheat", "#f5deb3") + val white = Color("white", "#ffffff") + val whiteSmoke = Color("whitesmoke", "#f5f5f5") + val yellow = Color("yellow", "#ffff00") + val yellowGreen = Color("yellowgreen", "#9acd32") + + fun normalizeFractionalPercent(value: Double): Double = + value.coerceIn(minimumValue = 0.0, maximumValue = 1.0) + + fun normalizePercent(value: Int): Int = + value.coerceIn(minimumValue = 0, maximumValue = 100) + + fun normalizeRGB(value: Int): Int = + value.coerceIn(minimumValue = 0, maximumValue = 255) + + // algorithm for capping from W3C + fun normalizeHue(value: Double): Int = + (((value % 360) + 360) % 360).roundToInt() + + fun normalizeAlpha(value: Double): Double = + normalizeFractionalPercent(value) + + // Match for hsl(int, int%, int%) | hsla(int, int%, int%, 0.5) | etc. + private val HSLA_REGEX by lazy { + Regex( + "^hsla?\\((-?[0-9]+\\.?[0-9]*(?:deg|grad|rad|turn)?)\\s*[, ]?\\s*(\\d{1,3})%\\s*[, ]\\s*(\\d{1,3})%\\s*[, ]?\\s*(\\d|(?:\\d?\\.\\d+))?\\)\$", + RegexOption.IGNORE_CASE + ) + } + + // Match for rgb(255, 255, 255) | rgba(255, 255, 255, 0.5) | rgb(100% 100% 100%) | etc. + private val RGBA_REGEX by lazy { + Regex( + "^rgba?\\((\\d{1,3}%?)\\s*[, ]\\s*(\\d{1,3}%?)\\s*[, ]\\s*(\\d{1,3}%?)[, ]?\\s*(\\d|(?:\\d?\\.\\d+))?\\)\$", + RegexOption.IGNORE_CASE + ) + } + } +} + +private fun String.withZeros() = this + "0".repeat(max(0, 3 - this.length)) +fun hex(value: Int) = Color("#${value.toString(16).withZeros()}") +fun rgb(red: Int, green: Int, blue: Int) = Color("rgb($red, $green, $blue)") +fun rgba(red: Int, green: Int, blue: Int, alpha: Double) = Color("rgba($red, $green, $blue, ${formatAlpha(alpha)})") +fun hsl(hue: Int, saturation: Int, lightness: Int) = Color("hsl($hue, $saturation%, $lightness%)") +fun hsla(hue: Int, saturation: Int, lightness: Int, alpha: Double) = Color("hsla($hue, $saturation%, $lightness%, ${formatAlpha(alpha)})") +fun blackAlpha(alpha: Double) = Color.black.withAlpha(alpha) +fun whiteAlpha(alpha: Double) = Color.white.withAlpha(alpha) + +private fun formatAlpha(alpha: Double): String = + alpha.toString().let { + if ("." in it) it else "$it.0" + } diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Common.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Common.kt new file mode 100644 index 0000000..c5d6377 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Common.kt @@ -0,0 +1,33 @@ +package nl.astraeus.css.properties + +class Length( + value: String +) : CssProperty(value) { + + companion object { + fun px(nr: Int) = Length("${nr}px") + fun em(nr: Int) = Length("${nr}em") + fun em(nr: Double) = Length("${nr}em") + fun perc(nr: Int) = Length("${nr}%") + fun perc(nr: Double) = Length("${nr}%") + fun pc(nr: Int) = Length("${nr}pc") + fun pc(nr: Double) = Length("${nr}pc") + fun cm(nr: Int) = Length("${nr}cm") + fun cm(nr: Double) = Length("${nr}cm") + val initial = Length("initial") + val inherit = Length("inherit") + } + +} + +class Fill( + value: String +) : CssProperty(value) { + + companion object { + val balance = Fill("balance") + val auto = Fill("auto") + val initial = Fill("initial") + val inherit = Fill("inherit") + } +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Content.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Content.kt new file mode 100644 index 0000000..5370272 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Content.kt @@ -0,0 +1,23 @@ +package nl.astraeus.css.properties + +class Content( + value: String +) : CssProperty(value) { + + companion object { + val normal = Content("normal") + val none = Content("none") + val counter = Content("counter") + val openQuote = Content("open-quote") + val closeQuote = Content("close-quote") + val noOpenQuote = Content("no-open-quote") + val noCloseQuote = Content("no-close-quote") + val initial = Content("initial") + val inherit = Content("inherit") + + fun attr(attribute: String) = Content("attr($attribute)") + fun string(txt: String) = Content("\"$txt\"") + fun url(url: String) = Content("url($url)") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Count.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Count.kt new file mode 100644 index 0000000..2cb6f7c --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Count.kt @@ -0,0 +1,15 @@ +package nl.astraeus.css.properties + +class Count( + value: String +) : CssProperty(value) { + + companion object { + val auto: Count = Count("auto") + val infinite: Count = Count("infinite") + val initial: Count = Count("initial") + val inherit: Count = Count("inherit") + + fun count(number: Int): Count = Count("$number") + } +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/CssFloat.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/CssFloat.kt new file mode 100644 index 0000000..4113f3e --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/CssFloat.kt @@ -0,0 +1,15 @@ +package nl.astraeus.css.properties + +class CssFloat( + value: String +) : CssProperty(value) { + + companion object { + val none = CssFloat("none") + val left = CssFloat("left") + val right = CssFloat("right") + val initial = CssFloat("initial") + val inherit = CssFloat("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/CssProperty.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/CssProperty.kt new file mode 100644 index 0000000..fa3f58a --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/CssProperty.kt @@ -0,0 +1,19 @@ +package nl.astraeus.css.properties + +interface CssValue { + fun css(): String +} + +open class CssProperty( + var value: String +) : CssValue { + + override fun css(): String = value + +} + +fun text(value: String) = TextProperty(value) + +class TextProperty( + value: String +) : CssProperty(value) diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/DelayDuration.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/DelayDuration.kt new file mode 100644 index 0000000..16533ca --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/DelayDuration.kt @@ -0,0 +1,14 @@ +package nl.astraeus.css.properties + +class DelayDuration( + value: String +) : CssProperty(value) { + + companion object { + val initial = DelayDuration("initial") + val inherit = DelayDuration("inherit") + + fun seconds(seconds: Int) = DelayDuration("${seconds}s") + fun millis(milliSeconds: Int) = DelayDuration("${milliSeconds}ms") + } +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Direction.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Direction.kt new file mode 100644 index 0000000..14cdf47 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Direction.kt @@ -0,0 +1,14 @@ +package nl.astraeus.css.properties + +class Direction( + value: String +) : CssProperty(value) { + + companion object { + val ltr = Direction("ltr") + val rtl = Direction("rtl") + val initial = Direction("initial") + val inherit = Direction("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Display.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Display.kt new file mode 100644 index 0000000..242305a --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Display.kt @@ -0,0 +1,33 @@ +package nl.astraeus.css.properties + +class Display( + value: String +) : CssProperty(value) { + + companion object { + val inline = Display("inline") + val block = Display("block") + val contents = Display("contents") + val flex = Display("flex") + val grid = Display("grid") + val inlineBlock = Display("inline-block") + val inlineFlex = Display("inline-flex") + val inlineGrid = Display("inline-grid") + val inlineTable = Display("inline-table") + val listItem = Display("list-item") + val runIn = Display("run-in") + val table = Display("table") + val tableCaption = Display("table-caption") + val tableColumnGroup = Display("table-column-group") + val tableHeaderGroup = Display("table-header-group") + val tableFooterGroup = Display("table-footer-group") + val tableRowGroup = Display("table-row-group") + val tableCell = Display("table-cell") + val tableColumn = Display("table-column") + val tableRow = Display("table-row") + val none = Display("none") + val initial = Display("initial") + val inherit = Display("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/EmptyCells.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/EmptyCells.kt new file mode 100644 index 0000000..9d43fac --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/EmptyCells.kt @@ -0,0 +1,14 @@ +package nl.astraeus.css.properties + +class EmptyCells( + value: String +) : CssProperty(value) { + + companion object { + val show = EmptyCells("show") + val hide = EmptyCells("hide") + val initial = EmptyCells("initial") + val inherit = EmptyCells("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Flex.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Flex.kt new file mode 100644 index 0000000..4e4a936 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Flex.kt @@ -0,0 +1,43 @@ +package nl.astraeus.css.properties + +class FlexDirection( + value: String +) : CssProperty(value) { + + companion object { + val row = FlexDirection("row") + val rowReverse = FlexDirection("row-reverse") + val column = FlexDirection("column") + val columnReverse = FlexDirection("column-reverse") + val initial = FlexDirection("initial") + val inherit = FlexDirection("inherit") + } + +} + +class FlexGrowShrink( + value: String +) : CssProperty(value) { + + companion object { + val initial = FlexGrowShrink("initial") + val inherit = FlexGrowShrink("inherit") + + fun number(number: Int) = FlexGrowShrink("$number") + } + +} + +class FlexWrap( + value: String +) : CssProperty(value) { + + companion object { + val nowrap = FlexWrap("nowrap") + val wrap = FlexWrap("wrap") + val wrapReverse = FlexWrap("wrap-reverse") + val initial = FlexWrap("initial") + val inherit = FlexWrap("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Font.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Font.kt new file mode 100644 index 0000000..2000c6e --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Font.kt @@ -0,0 +1,142 @@ +package nl.astraeus.css.properties + +class FontSize( + value: String +) : CssProperty(value) { + + companion object { + val xxSmall = FontSize("xx-small") + val xSmall = FontSize("x-small") + val small = FontSize("small") + val medium = FontSize("medium") + val large = FontSize("large") + val xLarge = FontSize("x-large") + val xxLarge = FontSize("xx-large") + val smaller = FontSize("smaller") + val larger = FontSize("larger") + val initial = FontSize("initial") + val inherit = FontSize("inherit") + + fun px(nr: Int) = FontSize("${nr}px") + fun em(nr: Int) = FontSize("${nr}em") + fun em(nr: Double) = FontSize("${nr}em") + fun perc(nr: Int) = FontSize("${nr}%") + fun perc(nr: Double) = FontSize("${nr}%") + fun pc(nr: Int) = FontSize("${nr}pc") + fun pc(nr: Double) = FontSize("${nr}pc") + fun cm(nr: Int) = FontSize("${nr}cm") + fun cm(nr: Double) = FontSize("${nr}cm") + } + +} + +class FontStretch( + value: String +) : CssProperty(value) { + + companion object { + val normal = FontStretch("normal") + val condensed = FontStretch("condensed") + val ultraCondensed = FontStretch("ultra-condensed") + val extraCondensed = FontStretch("extra-condensed") + val semiCondensed = FontStretch("semi-condensed") + val expanded = FontStretch("expanded") + val semiExpanded = FontStretch("semi-expanded") + val extraExpanded = FontStretch("extra-expanded") + val ultraExpanded = FontStretch("ultra-expanded") + val initial = FontWeight("initial") + val inherit = FontWeight("inherit") + } + +} + +class FontStyle( + value: String +) : CssProperty(value) { + + companion object { + val normal = FontStyle("normal") + val italic = FontStyle("italic") + val oblique = FontStyle("oblique") + val initial = FontStyle("initial") + val inherit = FontStyle("inherit") + } + +} + +class FontWeight( + value: String +) : CssProperty(value) { + + companion object { + val normal = FontWeight("normal") + val bold = FontWeight("bold") + val _100 = FontWeight("100") + val _200 = FontWeight("200") + val _300 = FontWeight("300") + val _400 = FontWeight("400") + val _500 = FontWeight("500") + val _600 = FontWeight("600") + val _700 = FontWeight("700") + val _800 = FontWeight("800") + val _900 = FontWeight("900") + val initial = FontWeight("initial") + val inherit = FontWeight("inherit") + } + +} + +class FontKerning( + value: String +) : CssProperty(value) { + + companion object { + val auto = FontKerning("auto") + val normal = FontKerning("normal") + val none = FontKerning("none") + } + +} + +class FontSizeAdjust( + value: String +) : CssProperty(value) { + + companion object { + val none = FontSizeAdjust("none") + val initial = FontSizeAdjust("initial") + val inherit = FontSizeAdjust("inherit") + } + +} + +class FontVariant( + value: String +) : CssProperty(value) { + + companion object { + val normal = FontVariant("normal") + val smallCaps = FontVariant("small-caps") + val initial = FontVariant("initial") + val inherit = FontVariant("inherit") + } + +} + +class FontVariantCaps( + value: String +) : CssProperty(value) { + + companion object { + val normal = FontVariantCaps("normal") + val smallCaps = FontVariantCaps("small-caps") + val allSmallCaps = FontVariantCaps("all-small-caps") + val petiteCaps = FontVariantCaps("petite-caps") + val allPetiteCaps = FontVariantCaps("all-petite-caps") + val unicase = FontVariantCaps("unicase") + val initial = FontVariantCaps("initial") + val inherit = FontVariantCaps("inherit") + val unset = FontVariantCaps("unset") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Grid.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Grid.kt new file mode 100644 index 0000000..67880ab --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Grid.kt @@ -0,0 +1,72 @@ +package nl.astraeus.css.properties + + +class Grid( + value: String +) : CssProperty(value) { + + companion object { + val none = Grid("none") + val initial = Grid("initial") + val inherit = Grid("inherit") + } + +} + +class GridAuto( + value: String +) : CssProperty(value) { + + companion object { + val auto = GridAuto("auto") + val maxContent = GridAuto("max-content") + val minContent = GridAuto("min-content") + } + +} + +class GridFlow( + value: String +) : CssProperty(value) { + + companion object { + val row = GridFlow("row") + val column = GridFlow("column") + val dense = GridFlow("dense") + val rowDense = GridFlow("row dense") + val columnDense = GridFlow("column dense") + } + +} + +class GridValue( + value: String +) : CssProperty(value) { + + companion object { + val auto = GridValue("auto") + + fun span(column: Int) = GridValue("span $column") + fun column(line: Int) = GridValue("$line") + fun row(line: Int) = GridValue("$line") + } + +} + +class TemplateRowColumn( + value: String +) : CssProperty(value) { + + companion object { + val none = GridValue("none") + val auto = GridValue("auto") + val maxContent = GridValue("max-content") + val minContent = GridValue("min-content") + val initial = GridValue("initial") + val inherit = GridValue("inherit") + + fun length(length: Measurement) = GridValue(length.value) + } + +} + diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Hyphens.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Hyphens.kt new file mode 100644 index 0000000..2ee01c9 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Hyphens.kt @@ -0,0 +1,15 @@ +package nl.astraeus.css.properties + +class Hyphens( + value: String +) : CssProperty(value) { + + companion object { + val none = Hyphens("none") + val manual = Hyphens("manual") + val auto = Hyphens("auto") + val initial = Hyphens("initial") + val inherit = Hyphens("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Image.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Image.kt new file mode 100644 index 0000000..6fe9544 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Image.kt @@ -0,0 +1,62 @@ +package nl.astraeus.css.properties + +class Image( + value: String +) : CssProperty(value) { + + companion object { + val none = Image("none") + val initial = Image("initial") + val inherit = Image("inherit") + + fun url(url: String) = Image("url($url)") + } +} + +class ImageRepeat( + value: String +) : CssProperty(value) { + + companion object { + val repeat = ImageRepeat("repeat") + val round = ImageRepeat("round") + val initial = ImageRepeat("initial") + val inherit = ImageRepeat("inherit") + + fun stretch(url: String) = ImageRepeat("stretch") + } + +} + +class ImageSlice( + value: String +) : CssProperty(value) { + + companion object { + val repeat = ImageSlice("repeat") + val fill = ImageSlice("fill") + val initial = ImageSlice("initial") + val inherit = ImageSlice("inherit") + + fun nr(nr: Int) = ImageSlice("$nr") + fun perc(perc: Int) = ImageSlice("$perc%") + fun perc(perc: Double) = ImageSlice("$perc%") + fun stretch(url: String) = ImageSlice("stretch") + } + +} + +class ImageSource( + value: String +) : CssProperty(value) { + + companion object { + val none = ImageSource("none") + val initial = ImageSource("initial") + val inherit = ImageSource("inherit") + + fun text(txt: String) = ImageSource(txt) + fun image(url: String) = ImageSource("'$url'") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/InitialInherit.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/InitialInherit.kt new file mode 100644 index 0000000..4fd3e23 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/InitialInherit.kt @@ -0,0 +1,12 @@ +package nl.astraeus.css.properties + +class InitialInherit( + value: String +) : CssProperty(value) { + + companion object { + val initial = InitialInherit("initial") + val inherit = InitialInherit("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Isolation.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Isolation.kt new file mode 100644 index 0000000..52e767f --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Isolation.kt @@ -0,0 +1,14 @@ +package nl.astraeus.css.properties + +class Isolation( + value: String +) : CssProperty(value) { + + companion object { + val auto = Isolation("auto") + val isolate = Isolation("isolate") + val initial = Isolation("initial") + val inherit = Isolation("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/JustifyContent.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/JustifyContent.kt new file mode 100644 index 0000000..5e2d79e --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/JustifyContent.kt @@ -0,0 +1,17 @@ +package nl.astraeus.css.properties + +class JustifyContent( + value: String +) : CssProperty(value) { + + companion object { + val flexStart = JustifyContent("flex-start") + val flexEnd = JustifyContent("flex-end") + val center = JustifyContent("center") + val spaceBetween = JustifyContent("space-between") + val spaceAround = JustifyContent("space-around") + val initial = JustifyContent("initial") + val inherit = JustifyContent("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/LetterSpacing.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/LetterSpacing.kt new file mode 100644 index 0000000..2ada69e --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/LetterSpacing.kt @@ -0,0 +1,13 @@ +package nl.astraeus.css.properties + +class LetterSpacing( + value: String +) : CssProperty(value) { + + companion object { + val normal = LetterSpacing("normal") + val initial = LetterSpacing("initial") + val inherit = LetterSpacing("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/ListStyle.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/ListStyle.kt new file mode 100644 index 0000000..91b7b7a --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/ListStyle.kt @@ -0,0 +1,48 @@ +package nl.astraeus.css.properties + + +class ListStylePosition( + value: String +) : CssProperty(value) { + + companion object { + val inside = ListStylePosition("inside") + val outside = ListStylePosition("outside") + val initial = ListStylePosition("initial") + val inherit = ListStylePosition("inherit") + } + +} + +class ListStyleType( + value: String +) : CssProperty(value) { + + companion object { + val disc = ListStyleType("disc") + val armenian = ListStyleType("armenian") + val circle = ListStyleType("circle") + val cjkIdeographic = ListStyleType("cjk-ideographic") + val decimal = ListStyleType("decimal") + val decimalLeadingZero = ListStyleType("decimal-leading-zero") + val georgian = ListStyleType("georgian") + val hebrew = ListStyleType("hebrew") + val hiragana = ListStyleType("hiragana") + val hiraganaIroha = ListStyleType("hiragana-iroha") + val katakana = ListStyleType("katakana") + val katakanaIroha = ListStyleType("katakana-iroha") + val lowerAlpha = ListStyleType("lower-alpha") + val lowerGreek = ListStyleType("lower-greek") + val lowerLatin = ListStyleType("lower-latin") + val lowerRoman = ListStyleType("lower-roman") + val none = ListStyleType("none") + val square = ListStyleType("square") + val upperAlpha = ListStyleType("upper-alpha") + val upperGreek = ListStyleType("upper-greek") + val upperLatin = ListStyleType("upper-latin") + val upperRoman = ListStyleType("upper-roman") + val initial = ListStyleType("initial") + val inherit = ListStyleType("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Measurement.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Measurement.kt new file mode 100644 index 0000000..9231f62 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Measurement.kt @@ -0,0 +1,127 @@ +package nl.astraeus.css.properties + +enum class MeasurementUoM { + NONE, + PX, + EM, + REL, + REM, + PC, + PRC, + CM, + FR +} + +open class Measurement( + value: String, + val uom: MeasurementUoM = MeasurementUoM.NONE +) : CssProperty(value) { + + override fun toString(): String = super.value + + companion object { + val auto = Measurement("auto") + val initial = Measurement("initial") + val inherit = Measurement("inherit") + val normal = Measurement("normal") + + fun fromString(value:String): Measurement = when { + value == "0" -> Measurement("0", MeasurementUoM.PX) + value.endsWith("px") -> Measurement(value.slice(0..(value.length-2)), MeasurementUoM.PX) + value.endsWith("rel") -> Measurement(value.slice(0..(value.length-3)), MeasurementUoM.REL) + else -> { + TODO("Unable to parse $value") + } + } + + fun px(nr: Int) = if (nr == 0) { + Measurement( + "0", + MeasurementUoM.PX + ) + } else { + Measurement( + "${nr}px", + MeasurementUoM.PX + ) + } + + fun px(nr: Double) = nr.px + fun em(nr: Int) = nr.em + fun em(nr: Double) = nr.em + fun prc(nr: Int) = nr.prc + fun prc(nr: Double) = nr.prc + fun pc(nr: Int) = nr.pc + fun pc(nr: Double) = nr.pc + fun cm(nr: Int) = nr.cm + fun cm(nr: Double) = nr.cm + fun fr(nr: Int) = nr.fr + } +} + +val Int.px: Measurement + get() = Measurement( + "${this}${ + if (this == 0) { + "" + } else { + "px" + } + }", + MeasurementUoM.PX + ) +val Int.em: Measurement + get() = Measurement( + "${this}${ + if (this == 0) { + "" + } else { + "em" + } + }", + MeasurementUoM.EM + ) +val Int.rem: Measurement + get() = Measurement( + "${this}${ + if (this == 0) { + "" + } else { + "rem" + } + }", + MeasurementUoM.REM + ) +val Int.prc: Measurement + get() = Measurement("${this}%", MeasurementUoM.PRC) +val Int.pc: Measurement + get() = Measurement("${this}pc", MeasurementUoM.PC) +val Int.cm: Measurement + get() = Measurement("${this}cm", MeasurementUoM.CM) +val Int.fr: Measurement + get() = Measurement("${this}fr", MeasurementUoM.FR) + +fun Int.px(): Measurement = Measurement.px(this) + +val Double.px: Measurement + get() = Measurement("${this}px", MeasurementUoM.PX) +val Double.em: Measurement + get() = Measurement("${this}em", MeasurementUoM.EM) +val Double.rem: Measurement + get() = Measurement("${this}rem", MeasurementUoM.REM) +val Double.prc: Measurement + get() = Measurement("${this}%", MeasurementUoM.PRC) +val Double.pc: Measurement + get() = Measurement("${this}pc", MeasurementUoM.PC) +val Double.cm: Measurement + get() = Measurement("${this}cm", MeasurementUoM.CM) + +fun Double.px(): Measurement = Measurement.px(this) + +open class LineHeight(value: String) : CssProperty(value) { + companion object { + val normal = LineHeight("normal") + val initial = LineHeight("initial") + val inherit = LineHeight("inherit") + } +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/MixBlendMode.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/MixBlendMode.kt new file mode 100644 index 0000000..002d9af --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/MixBlendMode.kt @@ -0,0 +1,25 @@ +package nl.astraeus.css.properties + + +class MixBlendMode( + value: String +) : CssProperty(value) { + + companion object { + val normal = MixBlendMode("normal") + val multiply = MixBlendMode("multiply") + val screen = MixBlendMode("screen") + val overlay = MixBlendMode("overlay") + val darken = MixBlendMode("darken") + val lighten = MixBlendMode("lighten") + val colorDodge = MixBlendMode("color-dodge") + val colorBurn = MixBlendMode("color-burn") + val difference = MixBlendMode("difference") + val exclusion = MixBlendMode("exclusion") + val hue = MixBlendMode("hue") + val saturation = MixBlendMode("saturation") + val color = MixBlendMode("color") + val luminosity = MixBlendMode("luminosity") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/ObjectFit.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/ObjectFit.kt new file mode 100644 index 0000000..ea0d4bb --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/ObjectFit.kt @@ -0,0 +1,17 @@ +package nl.astraeus.css.properties + +class ObjectFit( + value: String +) : CssProperty(value) { + + companion object { + val fill = ObjectFit("fill") + val contain = ObjectFit("contain") + val cover = ObjectFit("cover") + val scaleDown = ObjectFit("scale-down") + val none = ObjectFit("none") + val initial = ObjectFit("initial") + val inherit = ObjectFit("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/OutlineWidth.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/OutlineWidth.kt new file mode 100644 index 0000000..51ca6e1 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/OutlineWidth.kt @@ -0,0 +1,14 @@ +package nl.astraeus.css.properties + +class OutlineWidth( + value: String +) : CssProperty(value) { + + companion object { + val thin = OutlineWidth("thin") + val medium = OutlineWidth("medium") + val thick = OutlineWidth("thick") + val initial = BorderWidth("initial") + val inherit = BorderWidth("inherit") + } +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Overflow.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Overflow.kt new file mode 100644 index 0000000..b17f2ae --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Overflow.kt @@ -0,0 +1,15 @@ +package nl.astraeus.css.properties + +class Overflow( + value: String +) : CssProperty(value) { + + companion object { + val visible = Overflow("visible") + val hidden = Overflow("hidden") + val scroll = Overflow("scroll") + val auto = Overflow("auto") + val initial = BorderWidth("initial") + val inherit = BorderWidth("inherit") + } +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/PageBreak.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/PageBreak.kt new file mode 100644 index 0000000..3344e38 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/PageBreak.kt @@ -0,0 +1,17 @@ +package nl.astraeus.css.properties + +class PageBreak( + value: String +) : CssProperty(value) { + + companion object { + val auto = PageBreak("auto") + val always = PageBreak("always") + val avoid = PageBreak("avoid") + val left = PageBreak("left") + val right = PageBreak("right") + val initial = PageBreak("initial") + val inherit = PageBreak("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Perspective.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Perspective.kt new file mode 100644 index 0000000..2dab621 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Perspective.kt @@ -0,0 +1,13 @@ +package nl.astraeus.css.properties + +class Perspective( + value: String +) : CssProperty(value) { + + companion object { + val none = Perspective("none") + val initial = Perspective("initial") + val inherit = Perspective("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/PointerEvents.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/PointerEvents.kt new file mode 100644 index 0000000..92083cc --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/PointerEvents.kt @@ -0,0 +1,14 @@ +package nl.astraeus.css.properties + +class PointerEvents( + value: String +) : CssProperty(value) { + + companion object { + val auto = PointerEvents("auto") + val none = PointerEvents("none") + val initial = PointerEvents("initial") + val inherit = PointerEvents("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Position.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Position.kt new file mode 100644 index 0000000..0750059 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Position.kt @@ -0,0 +1,17 @@ +package nl.astraeus.css.properties + +class Position( + value: String +) : CssProperty(value) { + + companion object { + val static = Position("static") + val absolute = Position("absolute") + val fixed = Position("fixed") + val relative = Position("relative") + val sticky = Position("sticky") + val initial = Position("initial") + val inherit = Position("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Punctuation.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Punctuation.kt new file mode 100644 index 0000000..a368305 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Punctuation.kt @@ -0,0 +1,17 @@ +package nl.astraeus.css.properties + +class HangingPunctuation( + value: String +) : CssProperty(value) { + + companion object { + val none = HangingPunctuation("none") + val first = HangingPunctuation("first") + val last = HangingPunctuation("last") + val allowEnd = HangingPunctuation("allow-end") + val forceEnd = HangingPunctuation("force-end") + val initial = HangingPunctuation("initial") + val inherit = HangingPunctuation("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Resize.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Resize.kt new file mode 100644 index 0000000..01cc12c --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Resize.kt @@ -0,0 +1,16 @@ +package nl.astraeus.css.properties + +class Resize( + value: String +) : CssProperty(value) { + + companion object { + val none = Resize("none") + val both = Resize("both") + val horizontal = Resize("horizontal") + val vertical = Resize("vertical") + val initial = Resize("initial") + val inherit = Resize("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/ScrollBehavior.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/ScrollBehavior.kt new file mode 100644 index 0000000..c129633 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/ScrollBehavior.kt @@ -0,0 +1,14 @@ +package nl.astraeus.css.properties + +class ScrollBehavior( + value: String +) : CssProperty(value) { + + companion object { + val auto = ScrollBehavior("auto") + val smooth = ScrollBehavior("smooth") + val initial = ScrollBehavior("initial") + val inherit = ScrollBehavior("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Span.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Span.kt new file mode 100644 index 0000000..0aa860e --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Span.kt @@ -0,0 +1,14 @@ +package nl.astraeus.css.properties + +class Span( + value: String +) : CssProperty(value) { + + companion object { + val none = Clip("none") + val all = Clip("all") + val initial = Clip("initial") + val inherit = Clip("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/TableLayout.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/TableLayout.kt new file mode 100644 index 0000000..a7a77cc --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/TableLayout.kt @@ -0,0 +1,14 @@ +package nl.astraeus.css.properties + +class TableLayout( + value: String +) : CssProperty(value) { + + companion object { + val auto = TableLayout("auto") + val fixed = TableLayout("fixed") + val initial = TableLayout("initial") + val inherit = TableLayout("auto") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/TextAlign.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/TextAlign.kt new file mode 100644 index 0000000..03b7b2e --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/TextAlign.kt @@ -0,0 +1,16 @@ +package nl.astraeus.css.properties + +class TextAlign( + value: String +) : CssProperty(value) { + + companion object { + val left = TextAlign("left") + val right = TextAlign("right") + val center = TextAlign("center") + val justify = TextAlign("justify") + val initial = TextAlign("initial") + val inherit = TextAlign("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/TextAlignLast.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/TextAlignLast.kt new file mode 100644 index 0000000..99de0cf --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/TextAlignLast.kt @@ -0,0 +1,19 @@ +package nl.astraeus.css.properties + +class TextAlignLast( + value: String +) : CssProperty(value) { + + companion object { + val auto = TextAlignLast("auto") + val left = TextAlignLast("left") + val right = TextAlignLast("right") + val center = TextAlignLast("center") + val justify = TextAlignLast("justify") + val start = TextAlignLast("start") + val end = TextAlignLast("end") + val initial = TextAlignLast("initial") + val inherit = TextAlignLast("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/TextDecorationLine.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/TextDecorationLine.kt new file mode 100644 index 0000000..5306aff --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/TextDecorationLine.kt @@ -0,0 +1,16 @@ +package nl.astraeus.css.properties + +class TextDecorationLine( + value: String +) : CssProperty(value) { + + companion object { + val none = TextDecorationLine("none") + val underline = TextDecorationLine("underline") + val overline = TextDecorationLine("overline") + val lineThrough = TextDecorationLine("line-through") + val initial = TextDecorationLine("initial") + val inherit = TextDecorationLine("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/TextDecorationStyle.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/TextDecorationStyle.kt new file mode 100644 index 0000000..ac70b85 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/TextDecorationStyle.kt @@ -0,0 +1,17 @@ +package nl.astraeus.css.properties + +class TextDecorationStyle( + value: String +) : CssProperty(value) { + + companion object { + val solid = TextDecorationStyle("solid") + val double = TextDecorationStyle("double") + val dotted = TextDecorationStyle("dotted") + val dashed = TextDecorationStyle("dashed") + val wavy = TextDecorationStyle("wavy") + val initial = TextDecorationStyle("initial") + val inherit = TextDecorationStyle("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/TextJustify.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/TextJustify.kt new file mode 100644 index 0000000..6634807 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/TextJustify.kt @@ -0,0 +1,16 @@ +package nl.astraeus.css.properties + +class TextJustify( + value: String +) : CssProperty(value) { + + companion object { + val auto = TextJustify("auto") + val interWord = TextJustify("inter-word") + val interCharacter = TextJustify("inter-character") + val none = TextJustify("none") + val initial = TextJustify("initial") + val inherit = TextJustify("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/TextTransform.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/TextTransform.kt new file mode 100644 index 0000000..de33735 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/TextTransform.kt @@ -0,0 +1,16 @@ +package nl.astraeus.css.properties + +class TextTransform( + value: String +) : CssProperty(value) { + + companion object { + val none = TextTransform("none") + val capitalize = TextTransform("capitalize") + val uppercase = TextTransform("uppercase") + val lowercase = TextTransform("lowercase") + val initial = TextTransform("initial") + val inherit = TextTransform("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/TimingFunction.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/TimingFunction.kt new file mode 100644 index 0000000..ec636b6 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/TimingFunction.kt @@ -0,0 +1,31 @@ +package nl.astraeus.css.properties + +class TimingFunction( + value: String +) : CssProperty(value) { + + companion object { + val linear = TimingFunction("linear") + val ease = TimingFunction("ease") + val easeIn = TimingFunction("ease-in") + val easeOut = TimingFunction("ease-out") + val easeInOut = TimingFunction("ease-in-out") + val stepStart = TimingFunction("step-start") + val stepEnd = TimingFunction("step-end") + val initial = TimingFunction("initial") + val inherit = TimingFunction("inherit") + + fun steps(steps: Int, start: Boolean) = TimingFunction( + "steps($steps, ${ + if (start) { + "start" + } else { + "end" + } + }" + ) + + fun cubicBezier(n1: Double, n2: Double, n3: Double, n4: Double) = TimingFunction("cubic-bezier($n1, $n2, $n3, $n4)") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Transform.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Transform.kt new file mode 100644 index 0000000..55f8f69 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Transform.kt @@ -0,0 +1,51 @@ +package nl.astraeus.css.properties + +class Transform( + value: String +) : CssProperty(value) { + + companion object { + val none = Transform("none") + val initial = Transform("initial") + val inherit = Transform("inherit") + + fun matrix( + n1: Double, + n2: Double, + n3: Double, + n4: Double, + n5: Double, + n6: Double + ) = Transform("matrix($n1, $n2, $n3, $n4, $n5, $n6)") + + fun matrix3d( + n01: Double, n02: Double, n03: Double, n04: Double, + n05: Double, n06: Double, n07: Double, n08: Double, + n09: Double, n10: Double, n11: Double, n12: Double, + n13: Double, n14: Double, n15: Double, n16: Double + ) = Transform( + "matrix3d($n01, $n02, $n03, $n04, $n05, $n06, $n07, $n08, $n09, $n10, $n11, $n12, $n13, $n14, $n15, $n16)" + ) + + fun translate(x: Double, y: Double) = Transform("translate($x, $y)") + fun translate3d(x: Double, y: Double, z: Double) = Transform("translate3d($x, $y, $z)") + fun translateX(x: Double) = Transform("translateX($x)") + fun translateY(y: Double) = Transform("translateY($y)") + fun translateZ(z: Double) = Transform("translateZ($z)") + fun scale(x: Double, y: Double) = Transform("scale($x, $y)") + fun scale3d(x: Double, y: Double, z: Double) = Transform("scale3d($x, $y, $z)") + fun scaleX(x: Double) = Transform("scaleX($x)") + fun scaleY(y: Double) = Transform("scaleY($y)") + fun scaleZ(z: Double) = Transform("scaleZ($z)") + fun rotate(angle: Double) = Transform("rotate($angle)") + fun rotate3d(x: Double, y: Double, z: Double, angle: Double) = Transform("scale3d($x, $y, $z, $angle") + fun rotateX(x: Double) = Transform("rotateX($x)") + fun rotateY(y: Double) = Transform("rotateY($y)") + fun rotateZ(z: Double) = Transform("rotateZ($z)") + fun skew(x: Double, y: Double) = Transform("skew($x, $y)") + fun skewX(x: Double) = Transform("skew($x)") + fun skewY(y: Double) = Transform("skew($y)") + fun perspective(length: Measurement) = Transform("perspective(${length.css()})") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/TransformStyle.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/TransformStyle.kt new file mode 100644 index 0000000..d017579 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/TransformStyle.kt @@ -0,0 +1,14 @@ +package nl.astraeus.css.properties + +class TransformStyle( + value: String +) : CssProperty(value) { + + companion object { + val flat = TransformStyle("flat") + val preserve3d = TransformStyle("preserve-3d") + val initial = TransformStyle("initial") + val inherit = TransformStyle("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/UnicodeBidi.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/UnicodeBidi.kt new file mode 100644 index 0000000..f8defaf --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/UnicodeBidi.kt @@ -0,0 +1,15 @@ +package nl.astraeus.css.properties + +class UnicodeBidi( + value: String +) : CssProperty(value) { + + companion object { + val normal = UnicodeBidi("normal") + val embed = UnicodeBidi("embed") + val bidiOverride = UnicodeBidi("bidi-override") + val initial = UnicodeBidi("initial") + val inherit = UnicodeBidi("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/UserSelect.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/UserSelect.kt new file mode 100644 index 0000000..3b1b714 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/UserSelect.kt @@ -0,0 +1,14 @@ +package nl.astraeus.css.properties + +class UserSelect( + value: String +) : CssProperty(value) { + + companion object { + val auto = UserSelect("auto") + val none = UserSelect("none") + val text = UserSelect("text") + val all = UserSelect("all") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/VerticalAlign.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/VerticalAlign.kt new file mode 100644 index 0000000..5b2a80a --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/VerticalAlign.kt @@ -0,0 +1,20 @@ +package nl.astraeus.css.properties + +class VerticalAlign( + value: String +) : CssProperty(value) { + + companion object { + val baseline = VerticalAlign("baseline") + val sub = VerticalAlign("sub") + val _super = VerticalAlign("super") + val top = VerticalAlign("top") + val textTop = VerticalAlign("text-top") + val middle = VerticalAlign("middle") + val bottom = VerticalAlign("bottom") + val textBottom = VerticalAlign("text-bottom") + val initial = VerticalAlign("initial") + val inherit = VerticalAlign("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/Visibility.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/Visibility.kt new file mode 100644 index 0000000..b737f2f --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/Visibility.kt @@ -0,0 +1,15 @@ +package nl.astraeus.css.properties + +class Visibility( + value: String +) : CssProperty(value) { + + companion object { + val visible = Visibility("visible") + val hidden = Visibility("hidden") + val collapse = Visibility("collapse") + val initial = Visibility("initial") + val inherit = Visibility("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/WhiteSpace.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/WhiteSpace.kt new file mode 100644 index 0000000..8b50f10 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/WhiteSpace.kt @@ -0,0 +1,17 @@ +package nl.astraeus.css.properties + +class WhiteSpace( + value: String +) : CssProperty(value) { + + companion object { + val normal = WhiteSpace("normal") + val nowrap = WhiteSpace("nowrap") + val pre = WhiteSpace("pre") + val preLine = WhiteSpace("pre-line") + val preWrap = WhiteSpace("pre-wrap") + val initial = WhiteSpace("initial") + val inherit = WhiteSpace("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/WordBreak.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/WordBreak.kt new file mode 100644 index 0000000..1a5d0cc --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/WordBreak.kt @@ -0,0 +1,16 @@ +package nl.astraeus.css.properties + +class WordBreak( + value: String +) : CssProperty(value) { + + companion object { + val normal = WordBreak("normal") + val breakAll = WordBreak("break-all") + val keepAll = WordBreak("keep-all") + val breakWord = WordBreak("break-word") + val initial = WordBreak("initial") + val inherit = WordBreak("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/WordSpacing.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/WordSpacing.kt new file mode 100644 index 0000000..bf97db3 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/WordSpacing.kt @@ -0,0 +1,13 @@ +package nl.astraeus.css.properties + +class WordSpacing( + value: String +) : CssProperty(value) { + + companion object { + val normal = WordSpacing("normal") + val initial = WordSpacing("initial") + val inherit = WordSpacing("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/WordWrap.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/WordWrap.kt new file mode 100644 index 0000000..54b5529 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/WordWrap.kt @@ -0,0 +1,14 @@ +package nl.astraeus.css.properties + +class WordWrap( + value: String +) : CssProperty(value) { + + companion object { + val normal = WordWrap("normal") + val breakWord = WordWrap("break-word") + val initial = WordWrap("initial") + val inherit = WordWrap("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/WritingMode.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/WritingMode.kt new file mode 100644 index 0000000..9ce2b37 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/WritingMode.kt @@ -0,0 +1,13 @@ +package nl.astraeus.css.properties + +class WritingMode( + value: String +) : CssProperty(value) { + + companion object { + val horizontalTb = WritingMode("horizontal-tb") + val verticalRl = WritingMode("vertical-rl") + val verticalLr = WritingMode("vertical-lr") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/properties/ZIndex.kt b/src/commonMain/kotlin/nl/astraeus/css/properties/ZIndex.kt new file mode 100644 index 0000000..84b8c54 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/properties/ZIndex.kt @@ -0,0 +1,13 @@ +package nl.astraeus.css.properties + +class ZIndex( + value: String +) : CssProperty(value) { + + companion object { + val auto = ZIndex("auto") + val initial = ZIndex("initial") + val inherit = ZIndex("inherit") + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/style/CssBlock.kt b/src/commonMain/kotlin/nl/astraeus/css/style/CssBlock.kt new file mode 100644 index 0000000..dad6cd6 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/style/CssBlock.kt @@ -0,0 +1,6 @@ +package nl.astraeus.css.style + +data class CssBlock( + val selector: String, + val content: String +) diff --git a/src/commonMain/kotlin/nl/astraeus/css/style/CssFunctions.kt b/src/commonMain/kotlin/nl/astraeus/css/style/CssFunctions.kt new file mode 100644 index 0000000..e49c1d4 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/style/CssFunctions.kt @@ -0,0 +1,11 @@ +package nl.astraeus.css.style + +import nl.astraeus.css.properties.Color + +object CssFunctions { + + fun darken(color: Color, percentage: Int): Color { + return color + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/style/FontFace.kt b/src/commonMain/kotlin/nl/astraeus/css/style/FontFace.kt new file mode 100644 index 0000000..ee5d09b --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/style/FontFace.kt @@ -0,0 +1,42 @@ +package nl.astraeus.css.style + +import nl.astraeus.css.properties.CssProperty +import nl.astraeus.css.properties.FontSize +import nl.astraeus.css.properties.FontStretch +import nl.astraeus.css.properties.FontStyle +import nl.astraeus.css.properties.FontWeight + +@CssTagMarker +open class FontFace : CssGenerator() { + + override fun getValidator(name: String) = null + + fun fontFamily(font: String) { + props["font-family"] = listOf(CssProperty(font)) + } + + fun fontSize(size: FontSize) { + props["font-size"] = listOf(size) + } + + fun src(src: String) { + props["src"] = listOf(CssProperty(src)) + } + + fun fontStretch(stretch: FontStretch) { + props["font-stretch"] = listOf(stretch) + } + + fun fontStyle(style: FontStyle) { + props["font-style"] = listOf(style) + } + + fun fontWeight(weight: FontWeight) { + props["font-weight"] = listOf(weight) + } + + fun unicodeRange(unicodeRange: String) { + props["unicode-range"] = listOf(CssProperty(unicodeRange)) + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/style/KeyFrames.kt b/src/commonMain/kotlin/nl/astraeus/css/style/KeyFrames.kt new file mode 100644 index 0000000..191995c --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/style/KeyFrames.kt @@ -0,0 +1,16 @@ +package nl.astraeus.css.style + +@CssTagMarker +open class KeyFrames : CssGenerator() { + val frames: MutableMap = mutableMapOf() + + override fun getValidator(name: String): List? = listOf() + + fun percentage(percentage: Int, style: Css) { + val css = Style() + + style(css) + + frames[percentage] = style + } +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/style/Style.kt b/src/commonMain/kotlin/nl/astraeus/css/style/Style.kt new file mode 100644 index 0000000..9a4b04e --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/style/Style.kt @@ -0,0 +1,1587 @@ +package nl.astraeus.css.style + +import nl.astraeus.css.properties.* + +typealias Css = Style.() -> Unit + +typealias ConditionalCss = ConditionalStyle.() -> Unit + +@DslMarker +annotation class CssTagMarker + +private fun prp(vararg css: CssValue): List { + val result = mutableListOf() + + for (c in css) { + result.add(CssProperty(c.css())) + } + + return result +} + +private fun prp(vararg css: String): List { + val result = mutableListOf() + + for (c in css) { + result.add(CssProperty(c)) + } + + return result +} + +abstract class CssGenerator { + val definitions: MutableMap> = LinkedHashMap() + val props: MutableMap> = mutableMapOf() + + abstract fun getValidator(name: String): List? + + private fun propertyCss(indent: String, name: String, props: List): String { + val builder = StringBuilder() + + getValidator(name)?.forEach { + if (!it.validate(props)) { + println("Validate error '$name' - ${it.getMessage(name)}") + } + } + + for (prop in props) { + if (builder.isNotEmpty()) { + builder.append(" ") + } + builder.append(prop.css()) + } + + val paddedName = StringBuilder() + paddedName.append(indent) + paddedName.append(name) + paddedName.append(":") + while (paddedName.length < 32) { + paddedName.append(' ') + } + return "$paddedName$builder;\n" + } + + fun generatePropertyCss( + indent: String, + sortProperties: Boolean + ): String { + val builder = StringBuilder() + + if (sortProperties) { + for (name in props.keys.sorted()) { + val prop = props[name] ?: error("$name not found in properties after sorting!") + + builder.append(propertyCss(indent, name, prop)) + } + } else { + for ((name, prop) in props) { + builder.append(propertyCss(indent, name, prop)) + } + } + + return builder.toString() + } + + open fun generateCss( + indent: String = "", + minified: Boolean = false, + warnOnRedeclaration: Boolean = true, + allowCommaInSelector: Boolean = false, + combineEqualBlocks: Boolean = false, + sortProperties: Boolean = false + ): String { + val blocks = generateCssBlocks( + indent = indent, + minified = minified, + warnOnRedeclaration = warnOnRedeclaration, + allowCommaInSelector = allowCommaInSelector, + sortProperties = sortProperties + ) + + val builder = StringBuilder() + + fun StringBuilder.generateBlock( + indent: String, + selectors: List, + block: CssBlock? + ) { + if (selectors.isNotEmpty() && block != null) { + append(indent) + append(selectors.joinToString(",\n")) + append(" {\n") + append(block.content) + append(indent) + append("}\n\n") + } + } + + if (!combineEqualBlocks) { + var first = true + val selectors = mutableListOf() + var lastBlock: CssBlock? = null + + for (block in blocks) { + if (first) { + first = false + selectors.add(block.selector) + lastBlock = block + } else { + lastBlock = if (lastBlock != null && lastBlock.content == block.content) { + selectors.add(block.selector) + block + } else { + builder.generateBlock(indent, selectors, lastBlock) + + selectors.clear() + selectors.add(block.selector) + block + } + } + } + + builder.generateBlock(indent, selectors, lastBlock) + } else { + val blockHashes: MutableMap> = mutableMapOf() + + for (block in blocks) { + blockHashes.getOrPut(block.content.hashCode()) { + mutableListOf() + }.add(block) + } + val done = mutableSetOf() + + for(block in blocks) { + val hashCode = block.content.hashCode() + + if (!done.contains(hashCode)) { + blockHashes[hashCode]?.let { + val slctrs = it.map { blk -> + blk.selector + } + builder.generateBlock(indent, slctrs, block) + done.add(hashCode) + } + } + } + } + + return if (minified) { + val stripped = StringBuilder() + val skip = arrayOf(' ', '\t', '\n', '\r') + for (char in builder) { + if (!skip.contains(char)) { + stripped.append(char) + } + } + stripped.toString() + } else { + builder.toString() + } + } + + open fun generateCssBlocks( + namespace: String = "", + indent: String = "", + minified: Boolean = false, + warnOnRedeclaration: Boolean = true, + allowCommaInSelector: Boolean = false, + sortProperties: Boolean = false + ): List { + val blocks = mutableListOf() + + for (name in definitions.keys) { + val props = definitions[name]!! + val css = StringBuilder() + + if (warnOnRedeclaration && props.size > 1) { + css.append(" $indent/* style '$name' is defined ${props.size} times! */\n") + } + + val finalStyle = Style() + + for (prop in props) { + prop(finalStyle) + } + + css.append(finalStyle.generatePropertyCss(" $indent", sortProperties)) + + if (css.isNotBlank()) { + val builder = StringBuilder() + + check (allowCommaInSelector || !name.contains(',')) { + "Comma is not allowed in selector (option is set in generateCss call)" + } + + //builder.append("\n$namespace$name".trim()) + + //builder.append(" $indent") + //builder.append(" {\n") + + finalStyle.fontFace?.let { ff -> + builder.append(" $indent") + builder.append("@font-face {\n") + builder.append(ff.generatePropertyCss(" $indent", sortProperties)) + builder.append(" $indent") + builder.append("}\n") + } + + finalStyle.keyFrames.let { kf -> + kf.keys.sorted().forEach { frameName -> + val css = kf[frameName] + + builder.append(" $indent") + builder.append("@keyframes ") + builder.append(frameName) + builder.append(" {\n") + css?.let { css -> + for ((nr, style) in css.frames) { + builder.append(" $indent") + builder.append("${nr}% ") + builder.append(" $indent") + builder.append("{\n") + + val finalStyle = Style() + + style(finalStyle) + + builder.append(finalStyle.generatePropertyCss(" $indent", sortProperties)) + + builder.append(" $indent") + builder.append("}\n") + } + + builder.append(" $indent") + builder.append("}\n") + } + } + } + + builder.append(css) + //builder.append("}\n\n") + + blocks.add(CssBlock( + "$namespace$name".trim(), + builder.toString() + )) + } + + blocks.addAll(finalStyle.generateCssBlocks( + "$namespace$name".trim(), + indent, + minified = minified, + allowCommaInSelector = allowCommaInSelector, + warnOnRedeclaration = warnOnRedeclaration, + sortProperties = sortProperties + )) + } + + if (this is ConditionalStyle) { + this.media.let { mq -> + mq.keys.sorted().forEach { mediaName -> + val css = mq[mediaName] + + css?.let { css -> + val mediaStyle = ConditionalStyle() + + css(mediaStyle) + + blocks.add(CssBlock( + "$indent@media $mediaName".trim(), + mediaStyle.generateCss( + " $indent", + minified = minified, + allowCommaInSelector = allowCommaInSelector, + warnOnRedeclaration = warnOnRedeclaration, + sortProperties = sortProperties + ) + )) + } + } + } + + this.supports.let { mq -> + mq.keys.sorted().forEach { mediaName -> + val css = mq[mediaName] + + css?.let { css -> + val mediaStyle = ConditionalStyle() + + css(mediaStyle) + + blocks.add(CssBlock( + "$indent@supports $mediaName".trim(), + mediaStyle.generateCss( + " $indent", + minified = minified, + allowCommaInSelector = allowCommaInSelector, + warnOnRedeclaration = warnOnRedeclaration, + sortProperties = sortProperties + ) + )) + } + } + } + } + + return blocks + } +} + +interface DescriptionProvider { + fun description(): String +} + +class ValueDescriptionProvider( + val value: String +) : DescriptionProvider { + + override fun description() = value + +} + +fun txt(name: String): DescriptionProvider = ValueDescriptionProvider(name) +fun id(name: String): DescriptionProvider = ValueDescriptionProvider("#$name") +fun cls(name: String): DescriptionProvider = ValueDescriptionProvider(".$name") +fun attr(name: String): DescriptionProvider = ValueDescriptionProvider("[$name]") +fun attrEquals(name: String, value: String): DescriptionProvider = ValueDescriptionProvider("[$name=$value]") +fun attrContains(name: String, value: String): DescriptionProvider = ValueDescriptionProvider("[$name*=$value]") +fun attrEntriesContain(name: String, value: String): DescriptionProvider = ValueDescriptionProvider("[$name~=$value]") +fun attrEndsWith(name: String, value: String): DescriptionProvider = ValueDescriptionProvider("[$name$=$value]") +fun attrStartsWith(name: String, value: String): DescriptionProvider = ValueDescriptionProvider("[$name^=$value]") + +fun id(name: DescriptionProvider) = ValueDescriptionProvider("#${name.description()}") +fun cls(name: DescriptionProvider): DescriptionProvider = ValueDescriptionProvider(".${name.description()}") +fun attr(name: DescriptionProvider): DescriptionProvider = ValueDescriptionProvider("[${name.description()}]") +fun attrEquals(name: DescriptionProvider, value: String): DescriptionProvider = ValueDescriptionProvider("[${name.description()}=$value]") +fun attrContains(name: DescriptionProvider, value: String): DescriptionProvider = + ValueDescriptionProvider("[${name.description()}*=$value]") + +fun attrEntriesContain(name: DescriptionProvider, value: String): DescriptionProvider = + ValueDescriptionProvider("[${name.description()}~=$value]") + +fun attrEndsWith(name: DescriptionProvider, value: String): DescriptionProvider = + ValueDescriptionProvider("[${name.description()}$=$value]") + +fun attrStartsWith(name: DescriptionProvider, value: String): DescriptionProvider = + ValueDescriptionProvider("[${name.description()}^=$value]") + +@CssTagMarker +open class Style : CssGenerator() { + var fontFace: FontFace? = null + var keyFrames: MutableMap = mutableMapOf() + + private val validators = mapOf>( + "background-position" to listOf(InitialInheritSingleValue()), + "background-size" to listOf(MaxCountValidator(2)), + "border-radius" to listOf( + MaxCountValidator(4), + InitialInheritSingleValue() + ), + + "animation-iteration-mode" to listOf(MaxCountValidator(4)), + "animation-timing-function" to listOf(MaxCountValidator(4)), + "border-image-repeat" to listOf(MaxCountValidator(2)), + "border-image-slice" to listOf(MaxCountValidator(4)), + "border-spacing" to listOf(MaxCountValidator(4)), + "border-style" to listOf(MaxCountValidator(4)) + ) + + override fun getValidator(name: String) = validators[name] + + private fun addStyle(selector: String, style: Css) { + definitions[selector] = definitions[selector] ?: mutableListOf() + definitions[selector]?.add(style) + } + + /** + * like the scss & + * @param selector blabla + */ + fun and(vararg selectors: DescriptionProvider, style: Css) { + for (selector in selectors) { + addStyle(selector.description(), style) + } + } + + fun and(vararg selectors: String, style: Css) { + for (selector in selectors) { + addStyle(selector, style) + } + } + + fun select(vararg selectors: DescriptionProvider, style: Css) { + for (selector in selectors) { + addStyle(" ${selector.description()}", style) + } + } + + fun select(vararg selectors: String, style: Css) { + for (selector in selectors) { + addStyle(" $selector", style) + } + } + + fun descendant(vararg descNames: DescriptionProvider, style: Css) { + for (descName in descNames) { + addStyle(" ${descName.description()}", style) + } + } + + fun descendant(vararg descNames: String, style: Css) { + for (descName in descNames) { + addStyle(" $descName", style) + } + } + + fun child(vararg childNames: DescriptionProvider, style: Css) { + for (childName in childNames) { + addStyle(" > ${childName.description()}", style) + } + } + + fun child(vararg childNames: String, style: Css) { + for (childName in childNames) { + addStyle(" > $childName", style) + } + } + + fun sibling(vararg childNames: DescriptionProvider, style: Css) { + for (childName in childNames) { + addStyle(" ~ ${childName.description()}", style) + } + } + + fun sibling(vararg childNames: String, style: Css) { + for (childName in childNames) { + addStyle(" ~ $childName", style) + } + } + + fun adjSibling(vararg childNames: DescriptionProvider, style: Css) { + for (childName in childNames) { + addStyle(" + ${childName.description()}", style) + } + } + + fun adjSibling(vararg childNames: String, style: Css) { + for (childName in childNames) { + addStyle(" + $childName", style) + } + } + + fun active(style: Css) { + addStyle(":active", style) + } + + fun focus(style: Css) { + addStyle(":focus", style) + } + + fun focusWithin(style: Css) { + addStyle(":focus-within", style) + } + + fun hover(style: Css) { + addStyle(":hover", style) + } + + fun firstChild(style: Css) { + addStyle(":first-child", style) + } + + fun lastChild(style: Css) { + addStyle(":last-child", style) + } + + fun pseudoElement(selector: DescriptionProvider, style: Css) { + addStyle(":${selector.description()}", style) + } + + fun pseudoChild(selector: DescriptionProvider, style: Css) { + addStyle("::${selector.description()}", style) + } + + fun visited(style: Css) { + addStyle(":visited", style) + } + + fun not(selector: DescriptionProvider, style: Css) { + addStyle(":not(${selector.description()})", style) + } + + fun plain(name: String, value: String) { + props[name] = prp(value) + } + + fun plain(name: String, value: CssProperty) { + props[name] = prp(value) + } + + fun alignContent(value: AlignContent) { + props["align-content"] = prp(value) + } + + fun alignItems(alignItems: AlignItems) { + props["align-items"] = prp(alignItems) + } + + fun all(all: All) { + props["all"] = prp(all) + } + + fun alignSelf(alignSelf: AlignSelf) { + props["align-self"] = prp(alignSelf) + } + + fun animation(text: String) { + props["animation"] = prp(text) + } + + fun animationDelay(delay: DelayDuration) { + props["animation-delay"] = prp(delay) + } + + fun animationDirection(direction: AnimationDirection) { + props["animation-direction"] = prp(direction) + } + + fun animationDuration(duration: DelayDuration) { + props["animation-duration"] = prp(duration) + } + + fun animationFillMode(fillMode: AnimationFillMode) { + props["animation-fill-mode"] = prp(fillMode) + } + + fun animationIterationMode(vararg iterationMode: Count) { + props["animation-iteration-mode"] = prp(*iterationMode) + } + + fun animationFrame(frame: AnimationFrame) { + props["animation-frame"] = prp(frame) + } + + fun animationName(vararg name: String) { + props["animation-name"] = prp(*name) + } + + fun animationPlayState(vararg state: AnimationPlayState) { + props["animation-play-state"] = prp(*state) + } + + fun animationTimingFunction(vararg timingFunction: TimingFunction) { + props["animation-timing-function"] = prp(*timingFunction) + } + + fun backfaceVisibility(backfaceVisibility: BackfaceVisibility) { + props[""] = prp(backfaceVisibility) + } + + fun background(background: String) { + props["background"] = prp(background) + } + + fun backgroundAttachment(attachment: BackgroundAttachment) { + props["background-attachment"] = prp(attachment) + } + + fun backgroundBlendMode(blendMode: BackgroundBlendMode) { + props["background-blend-mode"] = prp(blendMode) + } + + fun backgroundClip(clip: ClipOrigin) { + props["background-clip"] = prp(clip) + } + + fun backgroundColor(color: Color) { + props["background-color"] = prp(color) + } + + fun backgroundImage(image: Image) { + props["background-image"] = prp(image) + } + + fun backgroundImage(value: String) { + props["background-image"] = prp(value) + } + + fun backgroundOrigin(origin: ClipOrigin) { + props["background-origin"] = prp(origin) + } + + fun backgroundPosition(position: BackgroundPosition) { + props["background-position"] = prp(position) + } + + fun backgroundRepeat(repeat: BackgroundRepeat) { + props["background-repeat"] = prp(repeat) + } + + fun backgroundSize(vararg size: BackgroundSize) { + props["background-size"] = prp(*size) + } + + fun border(border: String) { + props["border"] = prp(border) + } + + fun border( + width: Measurement, + style: BorderStyle, + color: Color + ) { + props["border"] = prp(width, style, color) + } + + fun borderBottom(borderBottom: String) { + props["border-bottom"] = prp(borderBottom) + } + + fun borderBottomColor(color: Color) { + props["border-bottom-color"] = prp(color) + } + + fun borderBottomLeftRadius(vararg radius: Measurement) { + props["border-bottom-left-radius"] = prp(*radius) + } + + fun borderBottomRightRadius(vararg radius: Measurement) { + props["border-bottom-right-radius"] = prp(*radius) + } + + fun borderBottomStyle(style: BorderStyle) { + props["border-bottom-style"] = prp(style) + } + + fun borderBottomWidth(width: BorderWidth) { + props["border-bottom-width"] = prp(width) + } + + fun borderBottomWidth(width: Measurement) { + props["border-bottom-width"] = prp(width) + } + + fun borderCollapse(collapse: BorderCollapse) { + props["border-collapse"] = prp(collapse) + } + + fun borderColor(vararg color: Color) { + props["border-color"] = prp(*color) + } + + fun borderImage(image: String) { + props["border-image"] = prp(image) + } + + fun borderImageOutset(imageOutset: Length) { + props["border-image-outset"] = prp(imageOutset) + } + + fun borderImageRepeat(vararg repeat: ImageRepeat) { + props["border-image-repeat"] = prp(*repeat) + } + + fun borderImageSlice(vararg slice: ImageSlice) { + props["border-image-slice"] = prp(*slice) + } + + fun borderImageSource(vararg source: ImageSource) { + props["border-image-source"] = prp(*source) + } + + fun borderImageWidth(vararg width: BorderImageWidth) { + props["border-image-width"] = prp(*width) + } + + fun borderLeft(left: String) { + props["border-left"] = prp(left) + } + + fun borderLeftColor(color: Color) { + props["border-left-color"] = prp(color) + } + + fun borderLeftStyle(style: BorderStyle) { + props["border-left-style"] = prp(style) + } + + fun borderLeftWidth(width: BorderWidth) { + props["border-left-width"] = prp(width) + } + + fun borderLeftWidth(width: Measurement) { + props["border-left-width"] = prp(width) + } + + fun borderRadius(radius: Measurement) { + props["border-radius"] = prp(radius) + } + + fun borderRadius( + topLeftBottomRight: Measurement, + topRightBottomLeft: Measurement + ) { + props["border-radius"] = listOf( + topLeftBottomRight, topRightBottomLeft + ) + } + + fun borderRadius( + topLeft: Measurement, + topRightBottomLeft: Measurement, + bottomRight: Measurement + ) { + props["border-radius"] = listOf( + topLeft, topRightBottomLeft, bottomRight + ) + } + + fun borderRadius( + topLeft: Measurement, + topRight: Measurement, + bottomRight: Measurement, + bottomLeft: Measurement + ) { + props["border-radius"] = listOf( + topLeft, topRight, bottomRight, bottomLeft + ) + } + + fun borderRight(border: String) { + props["border-right"] = prp(border) + } + + fun borderRightColor(color: Color) { + props["border-right-color"] = prp(color) + } + + fun borderRightStyle(style: BorderStyle) { + props["border-right-style"] = prp(style) + } + + fun borderRightWidth(width: BorderWidth) { + props["border-right-width"] = prp(width) + } + + fun borderRightWidth(width: Measurement) { + props["border-right-width"] = prp(width) + } + + fun borderSpacing(vararg spacing: BorderSpacing) { + props["border-spacing"] = prp(*spacing) + } + + fun borderStyle(vararg style: BorderStyle) { + props["border-style"] = prp(*style) + } + + fun borderTop(border: String) { + props["border-top"] = prp(border) + } + + fun borderTopColor(color: Color) { + props["border-top-color"] = prp(color) + } + + fun borderTopLeftRadius(radius: Measurement) { + props["border-top-left-radius"] = prp(radius) + } + + fun borderTopRightRadius(radius: Measurement) { + props["border-top-right-radius"] = prp(radius) + } + + fun borderTopStyle(style: BorderStyle) { + props["border-top-style"] = prp(style) + } + + fun borderTopWidth(width: BorderWidth) { + props["border-top-width"] = prp(width) + } + + fun borderTopWidth(width: Measurement) { + props["border-top-width"] = prp(width) + } + + fun borderWidth(width: Measurement) { + props["border-width"] = prp(width) + } + + fun borderWidth(width: BorderWidth) { + props["border-width"] = prp(width) + } + + fun bottom(measurement: Measurement) { + props["bottom"] = prp(measurement) + } + + fun boxDecorationBreak(brk: BoxDecorationBreak) { + props["box-decoration-break"] = prp(brk) + } + + fun boxShadow(shadow: BoxShadow) { + props["box-shadow"] = prp(shadow) + } + + fun boxSizing(sizing: BoxSizing) { + props["box-sizing"] = prp(sizing) + } + + fun breakAfter(brk: Break) { + props["break-after"] = prp(brk) + } + + fun breakBefore(brk: Break) { + props["break-before"] = prp(brk) + } + + fun breakInside(brk: Break) { + props["break-inside"] = prp(brk) + } + + fun captionSide(side: CaptionSide) { + props["caption-side"] = prp(side) + } + + fun caretColor(color: Color) { + props["caret-color"] = prp(color) + } + + fun clear(clear: Clear) { + props["clear"] = prp(clear) + } + + fun clip(clip: Clip) { + props["clip"] = prp(clip) + } + + fun clipPath(path: ClipPath) { + props["clip-path"] = prp(path) + } + + fun color(color: Color) { + props["color"] = listOf(color) + } + + fun columnCount(count: Count) { + props["column-count"] = prp(count) + } + + fun columnFill(fill: Fill) { + props["column-fill"] = prp(fill) + } + + fun columnGap(gap: Measurement) { + props["column-gap"] = prp(gap) + } + + fun columnRule(rule: String) { + props["column-rule"] = prp(rule) + } + + fun columnRuleColor(color: Color) { + props["column-rule-color"] = prp(color) + } + + fun columnRuleStyle(style: BorderStyle) { + props["column-rule-style"] = prp(style) + } + + fun columnRuleWidth(width: Measurement) { + props["column-rule-width"] = prp(width) + } + + fun columnSpan(span: Span) { + props["column-span"] = prp(span) + } + + fun columnWidth(width: Measurement) { + props["column-width"] = prp(width) + } + + fun column(column: String) { + props["column"] = prp(column) + } + + fun content(content: Content) { + props["content"] = prp(content) + } + + fun counterIncrement(increment: String) { + props["counter-increment"] = prp(increment) + } + + fun counterReset(reset: String) { + props["counter-reset"] = prp(reset) + } + + fun cursor(cursor: String) { + props["cursor"] = prp(cursor) + } + + fun direction(direction: Direction) { + props["direction"] = prp(direction) + } + + fun display(display: Display) { + props["display"] = prp(display) + } + + fun emptyCells(cells: EmptyCells) { + props["empty-cells"] = prp(cells) + } + + fun filter(filter: String) { + props["filter"] = prp(filter) + } + + fun flex(flex: String) { + props["flex"] = prp(flex) + } + + fun flexBasis(basis: Measurement) { + props["flex-basis"] = prp(basis) + } + + fun flexDirection(direction: FlexDirection) { + props["flex-direction"] = prp(direction) + } + + fun flexFlow(flow: String) { + props["flex-flow"] = prp(flow) + } + + fun flexGrow(grow: FlexGrowShrink) { + props["flex-grow"] = prp(grow) + } + + fun flexShrink(shrink: FlexGrowShrink) { + props["flex-shrink"] = prp(shrink) + } + + fun flexWrap(wrap: FlexWrap) { + props["flex-wrap"] = prp(wrap) + } + + fun float(cssFloat: CssFloat) { + props["float"] = prp(cssFloat) + } + + fun font(font: String) { + props["font"] = prp(font) + } + + fun fontFace(face: FontFace.() -> Unit) { + fontFace = FontFace() + + face.invoke(fontFace!!) + } + + fun fontFamily(font: String) { + props["font-family"] = prp(font) + } + + fun fontFeatureSettings(vararg setting: String) { + props["font-feature-settings"] = prp(*setting) + } + + fun fontKerning(kerning: FontKerning) { + props["font-kerking"] = prp(kerning) + } + + fun fontSize(size: FontSize) { + props["font-size"] = prp(size) + } + + fun fontSize(size: Measurement) { + props["font-size"] = prp(size) + } + + fun fontSizeAdjust(number: Double) { + props["font-size-adjust"] = prp(CssProperty("$number")) + } + + fun fontSizeAdjust(adjust: FontSizeAdjust) { + props["font-size-adjust"] = prp(adjust) + } + + fun fontStretch(stretch: FontStretch) { + props["font-stretch"] = prp(stretch) + } + + fun fontStyle(style: FontStyle) { + props["font-style"] = prp(style) + } + + fun fontVariant(variant: FontVariant) { + props["font-variant"] = prp(variant) + } + + fun fontVariantCaps(caps: FontVariantCaps) { + props["font-variant-caps"] = prp(caps) + } + + fun fontWeight(weight: FontWeight) { + props["font-weight"] = prp(weight) + } + + fun grid(grid: String) { + props["grid"] = prp(grid) + } + + fun gridArea(area: String) { + props["grid-area"] = prp(area) + } + + fun gridAutoColumns(columns: GridAuto) { + props["grid-auto-columns"] = prp(columns) + } + + fun gridAutoFlow(flow: GridFlow) { + props["grid-auto-flow"] = prp(flow) + } + + fun gridAutoRows(autoRows: GridAuto) { + props["grid-auto-rows"] = prp(autoRows) + } + + fun gridAutoRows(size: Measurement) { + props["grid-auto-rows"] = prp(size) + } + + fun gridColumn(start: GridValue, end: GridValue) { + props["grid-column"] = prp(CssProperty("${start.css()}/${end.css()}")) + } + + fun gridColumnEnd(end: GridValue) { + props["grid-column-end"] = prp(end) + } + + fun gridColumnGap(gap: GridValue) { + props["grid-column-gap"] = prp(gap) + } + + fun gridColumnStart(start: GridValue) { + props["grid-column-start"] = prp(start) + } + + fun gridGap( + rowGap: Measurement = Measurement.px(0), + columnGap: Measurement = Measurement.px(0) + ) { + props["grid-gap"] = prp(rowGap, columnGap) + } + + fun gridRow(start: GridValue, end: GridValue) { + props["grid-row"] = prp(CssProperty("${start.css()}/${end.css()}")) + } + + fun gridRowEnd(end: GridValue) { + props["grid-row-end"] = prp(end) + } + + fun gridRowGap(gap: GridValue) { + props["grid-row-end"] = prp(gap) + } + + fun gridRowStart(start: GridValue) { + props["grid-row-start"] = prp(start) + } + + fun gridTemplate(template: String) { + props["grid-template"] = prp(template) + } + + fun gridTemplateAreas(template: String) { + props["grid-template-areas"] = prp(template) + } + + @Deprecated( + "Fixed type, use gridTemplateColumns instead", + ReplaceWith("gridTemplateColumns(columns)") + ) + fun gridTemplateColumn(vararg columns: TemplateRowColumn) { + props["grid-template-columns"] = prp(*columns) + } + + fun gridTemplateColumns(vararg columns: TemplateRowColumn) { + props["grid-template-columns"] = prp(*columns) + } + + fun gridTemplateColumns(vararg columns: Measurement) { + props["grid-template-columns"] = prp(*columns) + } + + fun gridTemplateRows(vararg rows: TemplateRowColumn) { + props["grid-template-rows"] = prp(*rows) + } + + fun gridTemplateRows(vararg rows: Measurement) { + props["grid-template-rows"] = prp(*rows) + } + + fun hangingPunctuation(punctuation: Isolation) { + props["hanging-punctuation"] = prp(punctuation) + } + + fun height(height: Measurement) { + props["height"] = prp(height) + } + + fun hyphens(hyphens: Hyphens) { + props["hyphens"] = prp(hyphens) + } + + fun import(style: Css) { + style(this) + } + + fun isolation(isolation: Isolation) { + props["isolation"] = prp(isolation) + } + + fun justifyContent(content: JustifyContent) { + props["justify-content"] = prp(content) + } + + fun keyFrames(animationName: String, frames: KeyFrames.() -> Unit) { + val frameCss = KeyFrames() + + frames.invoke(frameCss) + + keyFrames[animationName] = frameCss + } + + fun left(left: Measurement) { + props["left"] = prp(left) + } + + fun letterSpacing(length: Measurement) { + props["letter-spacing"] = prp(length) + } + + fun letterSpacing(spacing: LetterSpacing) { + props["letter-spacing"] = prp(spacing) + } + + fun lineHeight(number: Double) { + props["line-height"] = prp("$number") + } + + fun lineHeight(measurement: Measurement) { + props["line-height"] = prp(measurement) + } + + fun lineHeight(height: LineHeight) { + props["line-height"] = prp(height) + } + + fun listStyle(style: String) { + props["list-style"] = prp(style) + } + + fun listStyleImage(url: String) { + props["list-style-image"] = prp("url('$url')") + } + + fun listStylePosition(position: ListStylePosition) { + props["list-style-position"] = prp(position) + } + + fun listStyleType(position: ListStyleType) { + props["list-style-type"] = prp(position) + } + + fun margin(all: Measurement) { + props["margin"] = prp(all) + } + + fun margin( + topBottom: Measurement, + leftRight: Measurement + ) { + props["margin"] = prp(topBottom, leftRight) + } + + fun margin(top: Measurement, right: Measurement, bottom: Measurement, left: Measurement) { + props["margin"] = prp(top, right, bottom, left) + } + + fun marginBottom(bottom: Measurement) { + props["margin-bottom"] = prp(bottom) + } + + fun marginLeft(left: Measurement) { + props["margin-left"] = prp(left) + } + + fun marginRight(right: Measurement) { + props["margin-right"] = prp(right) + } + + fun marginTop(top: Measurement) { + props["margin-top"] = prp(top) + } + + fun maxHeight(height: Measurement) { + props["max-height"] = prp(height) + } + + fun maxWidth(width: Measurement) { + props["max-width"] = prp(width) + } + + fun minHeight(height: Measurement) { + props["min-height"] = prp(height) + } + + fun minWidth(width: Measurement) { + props["min-width"] = prp(width) + } + + fun mixBlendMode(blendMode: MixBlendMode) { + props["mix-blend-mode"] = prp(blendMode) + } + + fun objectFit(fit: ObjectFit) { + props["object-fit"] = prp(fit) + } + + fun objectPosition(position: String) { + props["object-position"] = prp(position) + } + + fun opacity(opacity: Double) { + props["opacity"] = prp(opacity.toString()) + } + + fun opacity(opacity: InitialInherit) { + props["opacity"] = prp(opacity.toString()) + } + + fun order(order: Int) { + props["order"] = prp(order.toString()) + } + + fun order(order: InitialInherit) { + props["order"] = prp(order) + } + + fun outline(outline: String) { + props["outline"] = prp(outline) + } + + fun outlineColor(color: Color) { + props["outline-color"] = prp(color) + } + + fun outlineOffset(offset: Measurement) { + props["outline-offset"] = prp(offset) + } + + fun outlineStyle(style: BorderStyle) { + props["outline-style"] = prp(style) + } + + fun outlineWidth(width: OutlineWidth) { + props["outline-width"] = prp(width) + } + + fun outlineWidth(width: Measurement) { + props["outline-width"] = prp(width) + } + + fun overflow(overflow: Overflow) { + props["overflow"] = prp(overflow) + } + + fun overflowX(overflow: Overflow) { + props["overflow-x"] = prp(overflow) + } + + fun overflowY(overflow: Overflow) { + props["overflow-y"] = prp(overflow) + } + + fun padding(padding: Measurement) { + props["padding"] = prp(padding) + } + + fun padding(vertical: Measurement, horizontal: Measurement) { + props["padding"] = prp(vertical, horizontal) + } + + fun padding(top: Measurement, right: Measurement, bottom: Measurement, left: Measurement) { + props["padding"] = prp(top, right, bottom, left) + } + + fun padding(padding: InitialInherit) { + props["padding"] = prp(padding) + } + + fun paddingBottom(padding: Measurement) { + props["padding-bottom"] = prp(padding) + } + + fun paddingBottom(padding: InitialInherit) { + props["padding-bottom"] = prp(padding) + } + + fun paddingLeft(padding: Measurement) { + props["padding-left"] = prp(padding) + } + + fun paddingLeft(padding: InitialInherit) { + props["padding-left"] = prp(padding) + } + + fun paddingRight(padding: Measurement) { + props["padding-right"] = prp(padding) + } + + fun paddingRight(padding: InitialInherit) { + props["padding-right"] = prp(padding) + } + + fun paddingTop(padding: Measurement) { + props["padding-top"] = prp(padding) + } + + fun paddingTop(padding: InitialInherit) { + props["padding-top"] = prp(padding) + } + + fun pageBreakAfter(pageBreak: PageBreak) { + props["page-break-after"] = prp(pageBreak) + } + + fun pageBreakBefore(pageBreak: PageBreak) { + props["page-break-before"] = prp(pageBreak) + } + + fun pageBreakInside(pageBreak: PageBreak) { + props["page-break-inside"] = prp(pageBreak) + } + + fun perspective(length: Measurement) { + props["perspective"] = prp(length) + } + + fun perspective(perspective: Perspective) { + props["perspective"] = prp(perspective) + } + + fun perspectiveOrigin(po: String) { + props["perspective-origin"] = prp(po) + } + + fun pointerEvents(pe: PointerEvents) { + props["pointer-events"] = prp(pe) + } + + fun position(poition: Position) { + props["position"] = prp(poition) + } + + fun quotes(value: String) { + props["quotes"] = prp(value) + } + + fun resize(resize: Resize) { + props["resize"] = prp(resize) + } + + fun right(right: Measurement) { + props["right"] = prp(right) + } + + fun scrollBehavior(sb: ScrollBehavior) { + props["scroll-behavior"] = prp(sb) + } + + fun tabSize(number: Int) { + props["tab-size"] = prp(number.toString()) + } + + fun tabSize(length: Measurement) { + props["tab-size"] = prp(length) + } + + fun tabSize(ts: InitialInherit) { + props["tab-size"] = prp(ts) + } + + fun tableLayout(tl: TableLayout) { + props["table-layout"] = prp(tl) + } + + fun textAlign(ta: TextAlign) { + props["text-align"] = prp(ta) + } + + fun textAlignLast(tal: TextAlignLast) { + props["text-align-last"] = prp(tal) + } + + fun textDecoration(decoration: String) { + props["text-decoration"] = prp(decoration) + } + + fun textDecorationColor(color: Color) { + props["text-decoration-color"] = prp(color) + } + + fun textDecorationLine(tdc: TextDecorationLine) { + props["text-decoration-line"] = prp(tdc) + } + + fun textDecorationStyle(tds: TextDecorationStyle) { + props["text-decoration-style"] = prp(tds) + } + + fun textIndent(length: Measurement) { + props["text-indent"] = prp(length) + } + + fun textIndent(indent: InitialInherit) { + props["text-indent"] = prp(indent) + } + + fun textJustify(tj: TextJustify) { + props["text-justify"] = prp(tj) + } + + fun textOverflow(to: String) { + props["text-overflow"] = prp(to) + } + + fun textShadow(ts: String) { + props["text-shadow"] = prp(ts) + } + + fun textTransform(tt: TextTransform) { + props["text-transform"] = prp(tt) + } + + fun top(top: Measurement) { + props["top"] = prp(top) + } + + fun transform(transform: Transform) { + props["transform"] = prp(transform) + } + + fun transformOrigin(origin: String) { + props["transform-origin"] = prp(origin) + } + + fun transformStyle(style: TransformStyle) { + props["transform-style"] = prp(style) + } + + fun transition(transition: String) { + props["transition"] = prp(transition) + } + + fun transitionDelay(timeInSeconds: Double) { + props["transition-delay"] = prp("${timeInSeconds}s") + } + + fun transitionDelay(timeInMillis: Int) { + props["transition-delay"] = prp("${timeInMillis}ms") + } + + fun transitionDelay(delay: DelayDuration) { + props["transition-delay"] = prp(delay) + } + + fun transitionDuration(timeInSeconds: Double) { + props["transition-duration"] = prp("${timeInSeconds}s") + } + + fun transitionDuration(timeInMillis: Int) { + props["transition-duration"] = prp("${timeInMillis}ms") + } + + fun transitionDuration(td: DelayDuration) { + props["transition-duration"] = prp(td) + } + + fun transitionProperty(property: String) { + props["transition-property"] = prp(property) + } + + fun transitionTimingFunction(function: TimingFunction) { + props["transition-timing-function"] = prp(function) + } + + fun unicodeBidi(ub: UnicodeBidi) { + props["unicode-bidi"] = prp(ub) + } + + fun userSelect(us: UserSelect) { + props["user-select"] = prp(us) + } + + fun verticalAlign(length: Measurement) { + props["vertical-align"] = prp(length) + } + + fun verticalAlign(va: VerticalAlign) { + props["vertical-align"] = prp(va) + } + + fun visibility(visibility: Visibility) { + props["visibility"] = prp(visibility) + } + + fun whiteSpace(whiteSpace: WhiteSpace) { + props["white-space"] = prp(whiteSpace) + } + + fun width(width: Measurement) { + props["width"] = prp(width) + } + + fun wordBreak(wordBreak: WordBreak) { + props["word-break"] = prp(wordBreak) + } + + fun wordSpacing(wordSpacing: Measurement) { + props["word-spacing"] = prp(wordSpacing) + } + + fun wordSpacing(wordSpacing: WordSpacing) { + props["word-spacing"] = prp(wordSpacing) + } + + fun wordWrap(wordWrap: WordWrap) { + props["word-wrap"] = prp(wordWrap) + } + + fun writingMode(writingMode: WritingMode) { + props["writing-mode"] = prp(writingMode) + } + + fun zIndex(zIndex: Int) { + props["z-index"] = prp(zIndex.toString()) + } + + fun zIndex(zIndex: ZIndex) { + props["z-index"] = prp(zIndex) + } +} + +@CssTagMarker +open class ConditionalStyle : Style() { + var media: MutableMap = mutableMapOf() + var supports: MutableMap = mutableMapOf() + + fun media(definition: String, style: ConditionalCss) { + media[definition] = style + } + + fun supports(query: String, style: ConditionalCss) { + supports[query] = style + } +} diff --git a/src/commonMain/kotlin/nl/astraeus/css/style/Validator.kt b/src/commonMain/kotlin/nl/astraeus/css/style/Validator.kt new file mode 100644 index 0000000..79aaa1a --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/css/style/Validator.kt @@ -0,0 +1,37 @@ +package nl.astraeus.css.style + +import nl.astraeus.css.properties.CssProperty + +abstract class Validator { + + open fun validate(properties: List): Boolean = true + + open fun getMessage(name: String): String = "'$name' validation message not defined for $this" + +} + +class MaxCountValidator( + val number: Int +) : Validator() { + + override fun validate(property: List): Boolean = property.size <= number + + override fun getMessage(name: String): String = "'$name' should not have more than 4 entries" + +} + +class InitialInheritSingleValue : Validator() { + + override fun validate(properties: List): Boolean { + for (prop in properties) { + if (prop.css() == "initial" || prop.css() == "inherit") { + return properties.size == 1 + } + } + + return true + } + + override fun getMessage(name: String): String = "'$name' can only have single value when 'initial' or 'inherit'" + +} diff --git a/src/commonTest/kotlin/nl/astraeus/css/CssNameExample.kt b/src/commonTest/kotlin/nl/astraeus/css/CssNameExample.kt new file mode 100644 index 0000000..e2090f5 --- /dev/null +++ b/src/commonTest/kotlin/nl/astraeus/css/CssNameExample.kt @@ -0,0 +1,43 @@ +package nl.astraeus.css + +import nl.astraeus.css.properties.Color +import nl.astraeus.css.style.DescriptionProvider +import kotlin.test.Test + +private val CAPITAL_LETTER by lazy { Regex("[A-Z]") } + +fun String.hyphenize(): String = + replace(CAPITAL_LETTER) { + "-${it.value.lowercase()}" + } + +open class CssName(name: String? = null) : DescriptionProvider { + val name: String = if (name != null) { + "css-$name" + } else{ + "css${this::class.simpleName?.hyphenize() ?: this::class}" + } + + override fun description() = name +} + +object MainTitle : CssName() +object SectionTitle : CssName("sct-title") + +class CssNameExample { + + @Test + fun testCssName() { + val css = style { + select(MainTitle) { + color(Color.white) + } + + select(SectionTitle) { + color(Color.red) + } + } + + println(css.generateCss()) + } +} diff --git a/src/commonTest/kotlin/nl/astraeus/css/Examples.kt b/src/commonTest/kotlin/nl/astraeus/css/Examples.kt new file mode 100644 index 0000000..1f9e013 --- /dev/null +++ b/src/commonTest/kotlin/nl/astraeus/css/Examples.kt @@ -0,0 +1,131 @@ +package nl.astraeus.css + +import nl.astraeus.css.properties.BorderStyle +import nl.astraeus.css.properties.Color +import nl.astraeus.css.properties.Measurement +import nl.astraeus.css.properties.em +import nl.astraeus.css.properties.hsla +import nl.astraeus.css.properties.prc +import nl.astraeus.css.properties.px +import nl.astraeus.css.style.Style +import nl.astraeus.css.style.cls +import nl.astraeus.css.style.txt +import kotlin.test.Test + +class Examples { + + @Test + fun testColor() { + val color = hsla(0, 50, 50, 1.0) + val backgroundColor = Color.white + + val css = style { + select(cls("button")) { + padding(5.px) + + select("a") { + color(color) + backgroundColor(backgroundColor) + + hover { + color(color.lighten(10)) + backgroundColor(backgroundColor.darken(10)) + } + } + } + } + + println(css.generateCss(minified = true)) + } + + @Test + fun testMixins() { + fun Style.borderStyles(borderWidth: Measurement = 2.px) { + borderWidth(borderWidth) + borderColor(Color.aquamarine) + borderStyle(BorderStyle.solid) + } + + val css = style { + select(txt("a"), cls("button")) { + borderStyles() + + color(Color.white) + } + + select(cls("btn-primary")) { + borderStyles(3.px) + color(Color.blue) + } + } + + println(css.generateCss()) + } + + @Test + fun testMeasurements() { + val css = style { + select("body") { + fontSize(1.2.em) + borderWidth(3.px) + width(75.prc) + } + } + + println(css.generateCss()) + } + + @Test + fun testGeneration() { + val color = hsla(0, 50, 50, 1.0) + val backgroundColor = Color.white + + val css = style { + select(cls("button")) { + padding(5.px) + + select("a", "span") { + color(color) + backgroundColor(backgroundColor) + + hover { + color(color.lighten(10)) + backgroundColor(backgroundColor.darken(10)) + } + } + } + } + + println(css.generateCss( + minified = false, + sortProperties = true, + combineEqualBlocks = false + )) + } + + @Test + fun testMediaQueries() { + val css = style { + media("screen and (min-width: 30em)") { + select("html", "body") { + backgroundColor(Color.purple) + color(Color.blue) + } + } + + media("print") { + select("html", "body") { + backgroundColor(Color.white) + color(Color.darkGrey) + } + } + + } + + println(css.generateCss( + minified = false, + sortProperties = true, + combineEqualBlocks = true + )) + } +} diff --git a/src/commonTest/kotlin/nl/astraeus/css/TestCssBuilder.kt b/src/commonTest/kotlin/nl/astraeus/css/TestCssBuilder.kt new file mode 100644 index 0000000..7d47ad3 --- /dev/null +++ b/src/commonTest/kotlin/nl/astraeus/css/TestCssBuilder.kt @@ -0,0 +1,268 @@ +package nl.astraeus.css + +import nl.astraeus.css.properties.BoxSizing +import nl.astraeus.css.properties.Color +import nl.astraeus.css.properties.Count +import nl.astraeus.css.properties.Display +import nl.astraeus.css.properties.em +import nl.astraeus.css.properties.hsl +import nl.astraeus.css.properties.hsla +import nl.astraeus.css.properties.px +import nl.astraeus.css.properties.rgb +import nl.astraeus.css.properties.rgba +import nl.astraeus.css.style.attr +import nl.astraeus.css.style.attrEquals +import nl.astraeus.css.style.cls +import nl.astraeus.css.style.id +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class TestCssBuilder { + + @Test + fun testBuilder() { + val css = style { + + select("*", "*::before", "*::after") { + boxSizing(BoxSizing.borderBox) + } + + select("html") { + transition("background-color 1s ease") + + margin(0.px) + padding(0.px) + + focus { + backgroundColor(Color.blue) + } + } + + select("body") { + margin(0.px) + + padding(0.px) + + focus { + backgroundColor(Color.blue) + } + transition("background-color 1s ease") + } + + select(".test") { + top(10.px) + left(4.em) + backgroundColor(rgba(255, 255, 255, 0.75)) + animationIterationMode( + Count.auto, + Count.auto, + Count.auto, + Count.auto, + Count.auto + ) + + child("li") { + color(hsl(200, 50, 50)) + } + + select("> a") { + color(hsl(200, 50, 50)) + } + + hover { + color(Color.red) + } + + child("li") { + listStyle("none") + + child("ul") { + opacity(0.0) + display(Display.none) + paddingLeft(20.px) + child("li") { + listStyle("none") + + child("ul") { + paddingLeft(30.px) + child("li") { + listStyle("none") + } + } + } + } + + hover { + child("ul") { + opacity(1.0) + display(Display.block) + } + } + } + } + } + + println(css.generateCss(combineEqualBlocks = true, sortProperties = true)) + } + + @Test + fun testClass() { + val css2 = style { + select(id("my-label")) { + color(Color.antiqueWhite) + } + + select(cls("my-label")) { + color(Color.aliceBlue) + } + + // tr.even {} + select("tr") { + and(cls("even")) { + color(Color.gray) + } + +/* + nthChild(2) { + + } +*/ + + // not(bla) { + not(cls("bla")) { + color(Color.blue) + } + + } + + // table .even {} + select("tr") { + select(cls("even")) { + color(Color.green) + } + + // [type] + select(attr("type")) { + + } + + // [type="checkbox"] + select(attrEquals("type", "checkbox")) { + + } + + // table > .odd + child(cls("odd")) { + + } + + //adjSibling() + } + + select(cls("button")) { + fontSize(12.px) + color(hsl(200, 50, 50)) + + // .button:hover + hover { + color(hsl(200, 40, 40)) + } + + child(".green") { + color(Color.green) + } + + sibling(".red") { + color(Color.red) + } + + adjSibling(".blue") { + color(Color.blue) + } + } + } + + println(css2.generateCss()) + } + + + @Test + fun testOr() { + val css = style { + select("h1") { + color(Color.blue) + + select("table") { + color(Color.red) + + select("th", "td") { + color(Color.green) + } + } + } + } + + println(css.generateCss()) + } + + @Test + fun testOrWithComma() { + var excepted = false + try { + val css = style { + select("h1") { + color(Color.blue) + + select("table") { + color(Color.red) + + select("th, td") { + color(Color.green) + } + } + } + } + + println(css.generateCss()) + } catch (e: Exception) { + excepted = true + assertTrue { + e is IllegalStateException + } + assertTrue { + e.message?.contains("Comma is not allowed in selector") ?: false + } + } + assertTrue { + excepted + } + } + + @Test + fun testAlphaFunctions() { + val hsl = hsl(1, 50, 50) + val hsla = hsla(1, 50, 50, 0.5) + val rgb = rgb(101, 111, 121) + val rgba = rgba(100, 110, 120, 0.4) + val hex = Color("#88ff44") + val hexa = Color("#88ff4466") + + assertFalse { hsl.hasAlpha() } + assertFalse { rgb.hasAlpha() } + assertFalse { hex.hasAlpha() } + + assertTrue { hsla.hasAlpha() } + assertTrue { rgba.hasAlpha() } + assertTrue { hexa.hasAlpha() } + + assertEquals(0.5, hsla.getAlpha()) + assertEquals(0.4, rgba.getAlpha()) + assertEquals(0.5, hsla.getAlpha()) + assertEquals(0.4, hexa.getAlpha()) + assertEquals("646e78", rgba.toHex()) + assertEquals("bf4240", hsla.toHex()) + } + +}