├── .github
└── workflows
│ └── build_and_deploy_site.yml
├── .gitignore
├── .idea
├── .gitignore
├── misc.xml
└── vcs.xml
├── LICENSE
├── README.md
├── build.gradle.kts
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── parsing
├── build.gradle.kts
└── src
│ ├── commonMain
│ └── kotlin
│ │ └── io
│ │ └── github
│ │ └── opletter
│ │ └── css2kobweb
│ │ ├── Arg.kt
│ │ ├── CSSParser.kt
│ │ ├── ColoredCode.kt
│ │ ├── Css2Kobweb.kt
│ │ ├── ParseResults.kt
│ │ ├── PostProcessing.kt
│ │ ├── Properties.kt
│ │ ├── StringUtils.kt
│ │ ├── StyleModifier.kt
│ │ ├── constants
│ │ ├── Colors.kt
│ │ ├── CssRules.kt
│ │ ├── ShorthandProperties.kt
│ │ └── Units.kt
│ │ └── functions
│ │ ├── Color.kt
│ │ ├── ConicGradient.kt
│ │ ├── Gradient.kt
│ │ ├── LinearGradient.kt
│ │ ├── Position.kt
│ │ ├── RadialGradient.kt
│ │ └── Transition.kt
│ └── jvmMain
│ ├── kotlin
│ └── io
│ │ └── github
│ │ └── opletter
│ │ └── css2kobweb
│ │ ├── DataCreation.kt
│ │ └── Main.kt
│ └── resources
│ ├── colors.txt
│ └── units.txt
├── settings.gradle.kts
└── site
├── .gitignore
├── .kobweb
└── conf.yaml
├── build.gradle.kts
└── src
└── jsMain
├── kotlin
└── io
│ └── github
│ └── opletter
│ └── css2kobweb
│ ├── AppEntry.kt
│ ├── components
│ ├── layouts
│ │ └── PageLayout.kt
│ ├── sections
│ │ └── Footer.kt
│ ├── styles
│ │ └── Background.kt
│ └── widgets
│ │ └── KotlinCode.kt
│ └── pages
│ └── Index.kt
└── resources
└── public
└── favicon.ico
/.github/workflows/build_and_deploy_site.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Kobweb site to Pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | workflow_dispatch:
9 |
10 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
11 | permissions:
12 | contents: read
13 | pages: write
14 | id-token: write
15 |
16 | # Allow one concurrent deployment
17 | concurrency:
18 | group: "pages"
19 | cancel-in-progress: true
20 |
21 | jobs:
22 | export:
23 | runs-on: ubuntu-latest
24 | defaults:
25 | run:
26 | shell: bash
27 |
28 | env:
29 | KOBWEB_CLI_VERSION: 0.9.18
30 |
31 | steps:
32 | - name: Checkout
33 | uses: actions/checkout@v4
34 |
35 | - name: Set up Java
36 | uses: actions/setup-java@v4
37 | with:
38 | distribution: temurin
39 | java-version: 17
40 |
41 | - name: Setup Gradle
42 | uses: gradle/actions/setup-gradle@v4
43 | with:
44 | build-scan-publish: true
45 | build-scan-terms-of-use-url: "https://gradle.com/help/legal-terms-of-use"
46 | build-scan-terms-of-use-agree: "yes"
47 |
48 | - name: Query Browser Cache ID
49 | id: browser-cache-id
50 | run: echo "value=$(./gradlew -q :site:kobwebBrowserCacheId)" >> $GITHUB_OUTPUT
51 |
52 | - name: Cache Browser Dependencies
53 | uses: actions/cache@v4
54 | id: playwright-cache
55 | with:
56 | path: ~/.cache/ms-playwright
57 | key: ${{ runner.os }}-playwright-${{ steps.browser-cache-id.outputs.value }}
58 |
59 | - name: Fetch kobweb
60 | uses: robinraju/release-downloader@v1.10
61 | with:
62 | repository: "varabyte/kobweb-cli"
63 | tag: "v${{ env.KOBWEB_CLI_VERSION }}"
64 | fileName: "kobweb-${{ env.KOBWEB_CLI_VERSION }}.zip"
65 | tarBall: false
66 | zipBall: false
67 |
68 | - name: Unzip kobweb
69 | run: unzip kobweb-${{ env.KOBWEB_CLI_VERSION }}.zip
70 |
71 | - name: Run export
72 | run: |
73 | cd site
74 | ../kobweb-${{ env.KOBWEB_CLI_VERSION }}/bin/kobweb export --notty --layout static
75 |
76 | - name: Upload artifact
77 | uses: actions/upload-pages-artifact@v3
78 | with:
79 | path: ./site/.kobweb/site
80 |
81 | deploy:
82 | environment:
83 | name: github-pages
84 | url: ${{ steps.deployment.outputs.page_url }}
85 | runs-on: ubuntu-latest
86 | needs: export
87 | steps:
88 | - name: Deploy to GitHub Pages
89 | id: deployment
90 | uses: actions/deploy-pages@v4
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # General ignores
2 | .DS_Store
3 | build
4 | out
5 | kotlin-js-store
6 |
7 | # IntelliJ ignores
8 | *.iml
9 | /*.ipr
10 |
11 | /.idea/caches
12 | /.idea/libraries
13 | /.idea/modules.xml
14 | /.idea/workspace.xml
15 | /.idea/gradle.xml
16 | /.idea/navEditor.xml
17 | /.idea/assetWizardSettings.xml
18 | /.idea/artifacts
19 | /.idea/compiler.xml
20 | /.idea/jarRepositories.xml
21 | /.idea/*.iml
22 | /.idea/modules
23 | /.idea/libraries-with-intellij-classes.xml
24 |
25 | # Gradle ignores
26 | .gradle
27 |
28 | # Kotlin ignores
29 | .kotlin
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 opLetter
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a [Kobweb](https://github.com/varabyte/kobweb) project bootstrapped with the `app/empty` template.
2 |
3 | This template is useful if you already know what you're doing and just want a clean slate. By default, it
4 | just creates a blank home page (which prints to the console so you can confirm it's working)
5 |
6 | If you are still learning, consider instantiating the `app` template (or one of the examples) to see actual,
7 | working projects.
8 |
9 | ## Getting Started
10 |
11 | First, run the development server by typing the following command in a terminal under the `site` folder:
12 |
13 | ```bash
14 | $ cd site
15 | $ kobweb run
16 | ```
17 |
18 | Open [http://localhost:8080](http://localhost:8080) with your browser to see the result.
19 |
20 | You can use any editor you want for the project, but we recommend using **IntelliJ IDEA Community Edition** downloaded
21 | using the [Toolbox App](https://www.jetbrains.com/toolbox-app/).
22 |
23 | Press `Q` in the terminal to gracefully stop the server.
24 |
25 | ### Live Reload
26 |
27 | Feel free to edit / add / delete new components, pages, and API endpoints! When you make any changes, the site will
28 | indicate the status of the build and automatically reload when ready.
29 |
30 | ## Exporting the Project
31 |
32 | When you are ready to ship, you should shutdown the development server and then export the project using:
33 |
34 | ```bash
35 | kobweb export
36 | ```
37 |
38 | When finished, you can run a Kobweb server in production mode:
39 |
40 | ```bash
41 | kobweb run --env prod
42 | ```
43 |
44 | If you want to run this command in the Cloud provider of your choice, consider disabling interactive mode since nobody
45 | is sitting around watching the console in that case anyway. To do that, use:
46 |
47 | ```bash
48 | kobweb run --env prod --notty
49 | ```
50 |
51 | Kobweb also supports exporting to a static layout which is compatible with static hosting providers, such as GitHub
52 | Pages, Netlify, Firebase, any presumably all the others. You can read more about that approach here:
53 | https://bitspittle.dev/blog/2022/staticdeploy
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.kotlin.multiplatform) apply false
3 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 | org.gradle.caching=true
3 | org.gradle.configuration-cache=true
4 | #kotlin.js.ir.output.granularity=per-file
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | jetbrains-compose = "1.7.1"
3 | kobweb = "0.21.1"
4 | kotlin = "2.1.20"
5 |
6 | [libraries]
7 | compose-html-core = { module = "org.jetbrains.compose.html:html-core", version.ref = "jetbrains-compose" }
8 | compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "jetbrains-compose" }
9 | kobweb-core = { module = "com.varabyte.kobweb:kobweb-core ", version.ref = "kobweb" }
10 | kobweb-silk = { module = "com.varabyte.kobweb:kobweb-silk", version.ref = "kobweb" }
11 | silk-icons-fa = { module = "com.varabyte.kobwebx:silk-icons-fa", version.ref = "kobweb" }
12 |
13 | [plugins]
14 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
15 | kobweb-application = { id = "com.varabyte.kobweb.application", version.ref = "kobweb" }
16 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opLetter/css2kobweb/17386befdb5c76df2308fd484ba6a2dd3a006a40/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
90 | ' "$PWD" ) || exit
91 |
92 | # Use the maximum available, or set MAX_FD != -1 to use that value.
93 | MAX_FD=maximum
94 |
95 | warn () {
96 | echo "$*"
97 | } >&2
98 |
99 | die () {
100 | echo
101 | echo "$*"
102 | echo
103 | exit 1
104 | } >&2
105 |
106 | # OS specific support (must be 'true' or 'false').
107 | cygwin=false
108 | msys=false
109 | darwin=false
110 | nonstop=false
111 | case "$( uname )" in #(
112 | CYGWIN* ) cygwin=true ;; #(
113 | Darwin* ) darwin=true ;; #(
114 | MSYS* | MINGW* ) msys=true ;; #(
115 | NONSTOP* ) nonstop=true ;;
116 | esac
117 |
118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
119 |
120 |
121 | # Determine the Java command to use to start the JVM.
122 | if [ -n "$JAVA_HOME" ] ; then
123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
124 | # IBM's JDK on AIX uses strange locations for the executables
125 | JAVACMD=$JAVA_HOME/jre/sh/java
126 | else
127 | JAVACMD=$JAVA_HOME/bin/java
128 | fi
129 | if [ ! -x "$JAVACMD" ] ; then
130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
131 |
132 | Please set the JAVA_HOME variable in your environment to match the
133 | location of your Java installation."
134 | fi
135 | else
136 | JAVACMD=java
137 | if ! command -v java >/dev/null 2>&1
138 | then
139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
140 |
141 | Please set the JAVA_HOME variable in your environment to match the
142 | location of your Java installation."
143 | fi
144 | fi
145 |
146 | # Increase the maximum file descriptors if we can.
147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
148 | case $MAX_FD in #(
149 | max*)
150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
151 | # shellcheck disable=SC2039,SC3045
152 | MAX_FD=$( ulimit -H -n ) ||
153 | warn "Could not query maximum file descriptor limit"
154 | esac
155 | case $MAX_FD in #(
156 | '' | soft) :;; #(
157 | *)
158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
159 | # shellcheck disable=SC2039,SC3045
160 | ulimit -n "$MAX_FD" ||
161 | warn "Could not set maximum file descriptor limit to $MAX_FD"
162 | esac
163 | fi
164 |
165 | # Collect all arguments for the java command, stacking in reverse order:
166 | # * args from the command line
167 | # * the main class name
168 | # * -classpath
169 | # * -D...appname settings
170 | # * --module-path (only if needed)
171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
172 |
173 | # For Cygwin or MSYS, switch paths to Windows format before running java
174 | if "$cygwin" || "$msys" ; then
175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
177 |
178 | JAVACMD=$( cygpath --unix "$JAVACMD" )
179 |
180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
181 | for arg do
182 | if
183 | case $arg in #(
184 | -*) false ;; # don't mess with options #(
185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
186 | [ -e "$t" ] ;; #(
187 | *) false ;;
188 | esac
189 | then
190 | arg=$( cygpath --path --ignore --mixed "$arg" )
191 | fi
192 | # Roll the args list around exactly as many times as the number of
193 | # args, so each arg winds up back in the position where it started, but
194 | # possibly modified.
195 | #
196 | # NB: a `for` loop captures its iteration list before it begins, so
197 | # changing the positional parameters here affects neither the number of
198 | # iterations, nor the values presented in `arg`.
199 | shift # remove old arg
200 | set -- "$@" "$arg" # push replacement arg
201 | done
202 | fi
203 |
204 |
205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
207 |
208 | # Collect all arguments for the java command:
209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
210 | # and any embedded shellness will be escaped.
211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
212 | # treated as '${Hostname}' itself on the command line.
213 |
214 | set -- \
215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
216 | -classpath "$CLASSPATH" \
217 | org.gradle.wrapper.GradleWrapperMain \
218 | "$@"
219 |
220 | # Stop when "xargs" is not available.
221 | if ! command -v xargs >/dev/null 2>&1
222 | then
223 | die "xargs is not available"
224 | fi
225 |
226 | # Use "xargs" to parse quoted args.
227 | #
228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
229 | #
230 | # In Bash we could simply go:
231 | #
232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
233 | # set -- "${ARGS[@]}" "$@"
234 | #
235 | # but POSIX shell has neither arrays nor command substitution, so instead we
236 | # post-process each arg (as a line of input to sed) to backslash-escape any
237 | # character that might be a shell metacharacter, then use eval to reverse
238 | # that process (while maintaining the separation between arguments), and wrap
239 | # the whole thing up as a single "set" statement.
240 | #
241 | # This will of course break if any of these variables contains a newline or
242 | # an unmatched quote.
243 | #
244 |
245 | eval "set -- $(
246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
247 | xargs -n1 |
248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
249 | tr '\n' ' '
250 | )" '"$@"'
251 |
252 | exec "$JAVACMD" "$@"
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
--------------------------------------------------------------------------------
/parsing/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
2 |
3 | plugins {
4 | alias(libs.plugins.kotlin.multiplatform)
5 | }
6 |
7 | group = "io.github.opletter.css2kobweb"
8 | version = "1.0-SNAPSHOT"
9 |
10 | kotlin {
11 | jvm {
12 | @OptIn(ExperimentalKotlinGradlePluginApi::class)
13 | mainRun {
14 | mainClass = "io.github.opletter.css2kobweb.MainKt"
15 | }
16 | }
17 | js {
18 | browser()
19 | }
20 | }
--------------------------------------------------------------------------------
/parsing/src/commonMain/kotlin/io/github/opletter/css2kobweb/Arg.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb
2 |
3 | import io.github.opletter.css2kobweb.constants.units
4 |
5 | sealed class Arg(private val value: String) {
6 | class Literal(value: String) : Arg(value) {
7 | companion object {
8 | fun withQuotesIfNecessary(value: String): Literal {
9 | val str = if (value.firstOrNull() == '"') value else "\"$value\""
10 | return Literal(str)
11 | }
12 | }
13 | }
14 |
15 | sealed class FancyNumber(value: String) : Arg(value)
16 | class Hex(value: String) : FancyNumber("0x$value")
17 | class Float(value: Number) : FancyNumber("${value}f")
18 |
19 | /** A number that can be used in a calculation */
20 | sealed class CalcNumber(value: String) : Arg(value)
21 |
22 | class RawNumber(value: Number) : CalcNumber(value.toString())
23 |
24 | sealed class UnitNum(value: String) : CalcNumber(value) {
25 | class Normal(val value: Number, val type: String) :
26 | UnitNum("${if (value.toDouble() < 0.0) "($value)" else "$value"}.$type")
27 |
28 | class Calc(val arg1: CalcNumber, val arg2: CalcNumber, val operation: Char) : UnitNum(run {
29 | val arg1Str = arg1.let { if (it is Calc) "($it)" else "$it" }
30 | val arg2Str = arg2.let { if (it is Calc) "($it)" else "$it" }
31 | "$arg1Str $operation $arg2Str"
32 | })
33 |
34 | object Auto : UnitNum("autoLength")
35 |
36 | companion object {
37 | fun ofOrNull(str: String, zeroUnit: String = "px"): UnitNum? =
38 | parseCalcNum(str.prependCalcToParens(), zeroUnit) as? UnitNum
39 |
40 | fun of(str: String, zeroUnit: String = "px"): UnitNum {
41 | val unitNum = if (str == "auto") Auto else ofOrNull(str, zeroUnit)
42 | return requireNotNull(unitNum) { "Not a unit number: $str" }
43 | }
44 |
45 | private fun String.prependCalcToParens(): String = fold("") { result, c ->
46 | result + if (c == '(' && result.takeLast(4) != "calc") "calc$c" else c
47 | }
48 |
49 | private fun parseCalcNum(str: String, zeroUnit: String): CalcNumber? {
50 | if (str == "0") return Normal(0, zeroUnit)
51 |
52 | if (str.startsWith("calc(")) {
53 | // whitespace isn't required for / & *, so we add it for parsing (extra space gets trimmed anyway)
54 | val expr = parenContents(str)
55 | .replace("/", " / ")
56 | .replace("*", " * ")
57 | val parts = expr.splitNotInParens(' ')
58 |
59 | return when (parts.size) {
60 | 0, 2 -> null
61 | 1 -> parseCalcNum(parts.single(), zeroUnit)
62 | 3 -> {
63 | val (arg1, operation, arg2) = parts
64 | Calc(parseCalcNum(arg1, zeroUnit)!!, parseCalcNum(arg2, zeroUnit)!!, operation.single())
65 | }
66 |
67 | else -> {
68 | // For chained operations (e.g. "1px + 2px + 3px..."), we recursively add "calc(..)"
69 | // wrappings so that the rest of the parsing logic can handle it.
70 | val newCalc = parts.take(3).joinToString(" ", prefix = "calc(", postfix = ") ") +
71 | parts.drop(3).joinToString(" ")
72 | parseCalcNum("calc($newCalc)", zeroUnit)
73 | }
74 | }
75 | }
76 |
77 | val potentialUnit = str.dropWhile { it.isDigit() || it == '.' || it == '-' || it == '+' }.lowercase()
78 | val unit = units[potentialUnit]
79 | if (unit != null) {
80 | val num = str.dropLast(potentialUnit.length)
81 | return Normal(num.toIntOrNull() ?: num.toDouble(), unit)
82 | }
83 | return (str.toIntOrNull() ?: str.toDoubleOrNull())?.let { RawNumber(it) }
84 | }
85 | }
86 | }
87 |
88 | class Property(val className: String?, val value: String) : Arg(className?.let { "$it." }.orEmpty() + value) {
89 | companion object {
90 | fun fromKebabValue(className: String?, value: String) = Property(className, kebabToPascalCase(value))
91 | }
92 | }
93 |
94 | class NamedArg(val name: String, val value: Arg) : Arg("$name = $value")
95 |
96 | class Function(
97 | val name: String,
98 | val args: List = emptyList(),
99 | val lambdaStatements: List = emptyList(),
100 | ) : CssParseResult, Arg(
101 | if (lambdaStatements.isEmpty()) "$name(${args.joinToString(", ")})"
102 | else {
103 | val argsStr = if (args.isEmpty()) "" else args.joinToString(", ", prefix = "(", postfix = ")")
104 | val lambdaStr = lambdaStatements.joinToString("\n\t\t", prefix = " {\n\t\t", postfix = "\n\t}")
105 | name + argsStr + lambdaStr
106 | }
107 | ) {
108 | constructor(name: String, arg: Arg) : this(name, listOf(arg))
109 |
110 | override fun asCodeBlocks(indentLevel: Int): List = (this as Arg).asCodeBlocks(indentLevel)
111 |
112 | internal companion object // for extensions
113 | }
114 |
115 | class ExtensionCall(val property: Arg, val function: Function) : Arg("$property.$function")
116 |
117 | override fun toString(): String = value
118 | override fun hashCode(): Int = value.hashCode()
119 | override fun equals(other: Any?): Boolean = other is Arg && other.value == value
120 |
121 | internal companion object // for extensions
122 | }
123 |
124 |
125 | fun Arg.asCodeBlocks(
126 | indentLevel: Int,
127 | functionType: CodeElement = CodeElement.Plain,
128 | nestedCalc: Boolean = false,
129 | ): List {
130 | return when (this) {
131 | is Arg.Literal -> listOf(CodeBlock(toString(), CodeElement.String))
132 | is Arg.FancyNumber, is Arg.RawNumber -> listOf(CodeBlock(toString(), CodeElement.Number))
133 | is Arg.Property -> listOfNotNull(
134 | className?.let { CodeBlock("$it.", CodeElement.Plain) },
135 | CodeBlock(value, CodeElement.Property),
136 | )
137 |
138 | is Arg.UnitNum.Normal -> buildList {
139 | add(CodeBlock(value.toString(), CodeElement.Number))
140 | if (value.toDouble() < 0.0) {
141 | add(0, CodeBlock("(", CodeElement.Plain))
142 | add(CodeBlock(")", CodeElement.Plain))
143 | }
144 | add(CodeBlock(".", CodeElement.Plain))
145 | add(CodeBlock(type, CodeElement.Property))
146 | }
147 |
148 | is Arg.UnitNum.Calc -> buildList {
149 | addAll(arg1.asCodeBlocks(indentLevel, nestedCalc = true))
150 | add(CodeBlock(" $operation ", CodeElement.Plain))
151 | addAll(arg2.asCodeBlocks(indentLevel, nestedCalc = true))
152 |
153 | if (nestedCalc) {
154 | add(0, CodeBlock("(", CodeElement.Plain))
155 | add(CodeBlock(")", CodeElement.Plain))
156 | }
157 | }
158 |
159 | is Arg.UnitNum.Auto -> listOf(CodeBlock(toString(), CodeElement.Property))
160 |
161 | is Arg.NamedArg -> listOf(CodeBlock("$name = ", CodeElement.NamedArg)) + value.asCodeBlocks(indentLevel)
162 |
163 | is Arg.Function -> buildList {
164 | val indents = "\t".repeat(indentLevel)
165 | add(CodeBlock(name, functionType))
166 | if (args.isNotEmpty() || lambdaStatements.isEmpty()) {
167 | val longArgs = args.toString().length > 100 // number chosen arbitrarily
168 | val separator = if (longArgs) ",\n\t$indents" else ", "
169 | val start = if (longArgs) "(\n\t$indents" else "("
170 | val end = if (longArgs) "\n$indents)" else ")"
171 |
172 | add(CodeBlock(start, CodeElement.Plain))
173 | args.forEachIndexed { index, arg ->
174 | addAll(arg.asCodeBlocks(indentLevel + if (longArgs) 1 else 0))
175 | if (index < args.size - 1)
176 | add(CodeBlock(separator, CodeElement.Plain))
177 | }
178 | add(CodeBlock(end, CodeElement.Plain))
179 | }
180 | if (lambdaStatements.isNotEmpty()) {
181 | add(CodeBlock(" {", CodeElement.Plain))
182 | // todo: consider making this a setting
183 | val sameLine = lambdaStatements.size == 1 && lambdaStatements.single().toString().length < 50
184 |
185 | lambdaStatements.forEach {
186 | add(CodeBlock(if (sameLine) " " else "\n\t$indents", CodeElement.Plain))
187 | addAll(it.asCodeBlocks(indentLevel + 1))
188 | }
189 | add(CodeBlock(if (sameLine) " }" else "\n$indents}", CodeElement.Plain))
190 | }
191 | }
192 |
193 | is Arg.ExtensionCall -> {
194 | property.asCodeBlocks(indentLevel) + CodeBlock(".", CodeElement.Plain) +
195 | function.asCodeBlocks(indentLevel, functionType = CodeElement.ExtensionFun)
196 | }
197 | }
198 | }
--------------------------------------------------------------------------------
/parsing/src/commonMain/kotlin/io/github/opletter/css2kobweb/CSSParser.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb
2 |
3 | internal fun parseCss(css: String): List {
4 | return css.splitIntoCssBlocks().mapNotNull { (selector, properties) ->
5 | val subBlocks = properties.splitIntoCssBlocks()
6 |
7 | if (subBlocks.isEmpty()) {
8 | ParsedStyleBlock(getProperties(properties), selector)
9 | } else if (selector.startsWith("@keyframes")) {
10 | val modifiers = subBlocks.map { (subSelector, subProperties) ->
11 | ParsedStyleBlock(getProperties(subProperties), subSelector)
12 | }
13 | ParsedKeyframes(selector.substringAfter("@keyframes").trim(), modifiers)
14 | } else null // TODO: handle @media and maybe some other nested blocks?
15 | }
16 | }
17 |
18 | internal fun getProperties(str: String): List {
19 | return str.splitNotInParens(';').mapNotNull { prop ->
20 | val (name, value) = prop.split(':', limit = 2).map { it.trim() } + "" // use empty if not present
21 |
22 | if (name.startsWith("--")) return@mapNotNull null // ignore css variables
23 |
24 | val parsedProperty = if (name.startsWith("-")) {
25 | val propertyArgs = listOf(name, value).map { Arg.Literal.withQuotesIfNecessary(it) }
26 | Arg.Function("styleModifier", lambdaStatements = listOf(Arg.Function("property", propertyArgs)))
27 | } else {
28 | parseCssProperty(
29 | propertyName = kebabToCamelCase(name),
30 | value = value
31 | .replace("!important", "")
32 | .lines()
33 | .joinToString(" ") { it.trim() }
34 | .replace(" ", " "),
35 | )
36 | }
37 |
38 | parsedProperty.name to parsedProperty
39 | }.postProcessProperties()
40 | }
41 |
42 | /**
43 | * Returns a list of pairs of the form (selector, block content).
44 | * Note that this only gets the first level of selectors, so nested selectors will be kept within their parent.
45 | */
46 | private fun String.splitIntoCssBlocks(): List> {
47 | return splitNotBetween(setOf('{' to '}'), setOf('{'))
48 | .filter { it.isNotBlank() }
49 | .fold(listOf>()) { acc, str ->
50 | val prev = acc.lastOrNull() ?: ("" to "")
51 | val properties = str.substringBeforeLast("}").trim()
52 | val nextSelector = str.substringAfterLast("}").trim()
53 |
54 | acc.dropLast(1) + (prev.first to properties) + (nextSelector to "")
55 | }.drop(1).dropLast(1)
56 | }
--------------------------------------------------------------------------------
/parsing/src/commonMain/kotlin/io/github/opletter/css2kobweb/ColoredCode.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb
2 |
3 | enum class CodeElement {
4 | Plain, Keyword, Property, ExtensionFun, String, Number, NamedArg
5 | }
6 |
7 | class CodeBlock(val text: String, val type: CodeElement) {
8 | override fun toString(): String = text
9 | }
10 |
11 | fun css2kobwebAsCode(rawCSS: String, extractOutCommonModifiers: Boolean = true): List {
12 | // fold adjacent code blocks of the same type into one to hopefully improve rendering performance
13 | // we use a mutable list as otherwise this can become a performance bottleneck
14 | return css2kobweb(rawCSS, extractOutCommonModifiers).asCodeBlocks().toMutableList().apply {
15 | var i = 0
16 | while (i < size - 1) {
17 | if (this[i].type == this[i + 1].type) {
18 | this[i] = CodeBlock(this[i].text + this[i + 1].text, CodeElement.Plain)
19 | removeAt(i + 1)
20 | } else {
21 | i++
22 | }
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/parsing/src/commonMain/kotlin/io/github/opletter/css2kobweb/Css2Kobweb.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb
2 |
3 | import io.github.opletter.css2kobweb.constants.cssRules
4 |
5 | fun css2kobweb(rawCSS: String, extractOutCommonModifiers: Boolean = true): CssParseResult {
6 | val cleanedCss = inlineCssVariables(rawCSS)
7 | .lines()
8 | .filterNot { it.startsWith("@import") || it.startsWith("@charset") || it.startsWith("@namespace") }
9 | .joinToString("\n")
10 | .replace("/\\*[\\s\\S]*?\\*/".toRegex(), "") // remove comments
11 | .replace('\'', '"') // to simplify parsing
12 |
13 | val cssBySelector = parseCss(cleanedCss).ifEmpty {
14 | return if (":" in cleanedCss) {
15 | ParsedStyleBlock(getProperties(cleanedCss))
16 | } else {
17 | val parsedProperty = parseCssProperty("", cleanedCss)
18 | val singleArg = parsedProperty.args.singleOrNull()
19 | // Only return non-trivial parsed properties so that we don't show garbage during the initial input.
20 | // However, to show responsiveness, we do want to show some output ("Modifier") when the user start typing
21 | if (singleArg != null && (singleArg !is Arg.Property || singleArg.className != "")) {
22 | parsedProperty
23 | } else if (cleanedCss.trimEnd().last() == '{') {
24 | // display an empty CssStyle block if it looks like the css will have a selector
25 | ParsedCssStyles(listOf(ParsedCssStyle("", emptyMap())))
26 | } else {
27 | ParsedStyleBlock(emptyList())
28 | }
29 | }
30 | }
31 |
32 | val parsedModifiers = cssBySelector.filterIsInstance().run {
33 | // If there are only empty blocks, it's likely the user is still typing, so we show them
34 | // However if there are non-empty blocks, then we hide any empty blocks since they're not needed
35 | // Note that empty blocks may arise if the original block only contained css vars (which are inlined)
36 | filter { it.properties.isNotEmpty() }.ifEmpty { this }
37 | }
38 |
39 | val modifiersBySelector = parsedModifiers.flatMapIndexed { index, modifier ->
40 | val allSelectors = modifier.label.splitNotInParens(',')
41 |
42 | allSelectors.associateWith { _ ->
43 | if (extractOutCommonModifiers && allSelectors.distinctBy { it.baseName() }.size != 1) {
44 | StyleModifier.Global("sharedModifier$index", modifier)
45 | } else if (extractOutCommonModifiers && allSelectors.size != 1) {
46 | StyleModifier.Local("sharedModifier$index", modifier)
47 | } else {
48 | StyleModifier.Inline(modifier)
49 | }
50 | }.toList()
51 | }.sortedBy { it.first }.fold(emptyList>()) { acc, (selector, modifier) ->
52 | val prev = acc.lastOrNull()
53 | if (prev?.first == selector) {
54 | acc.dropLast(1) + (selector to (prev.second + modifier))
55 | } else {
56 | acc + (selector to modifier)
57 | }
58 | }.toMap()
59 |
60 | val styles = parsedModifiers.flatMap { it.label.splitNotInParens(',') }.groupBy { it.baseName() }
61 | val parsedStyles = styles.map { (baseName, selectors) ->
62 | val modifiers = selectors.associate { selector ->
63 | val cleanedUpName = if (selector == baseName) {
64 | "base"
65 | } else {
66 | selector.substringAfter(baseName).let {
67 | cssRules[it] ?: "cssRule(\"${it.replace("\"", "\\\"")}\")"
68 | }
69 | }
70 |
71 | cleanedUpName to modifiersBySelector[selector]!!
72 | }
73 | val styleName = kebabToPascalCase(baseName.substringAfter(".").substringAfter("#"))
74 | .replace("*", "All")
75 | ParsedCssStyle(styleName, modifiers)
76 | }.let { ParsedCssStyles(it) }
77 |
78 | val keyframes = cssBySelector.filterIsInstance()
79 |
80 | return if (keyframes.isEmpty()) parsedStyles else ParsedBlocks(parsedStyles, keyframes)
81 | }
82 |
83 | private fun inlineCssVariables(css: String): String {
84 | val cssVarPattern = Regex("--([\\w-]+):\\s*([^;]+);")
85 | var newCss = css
86 |
87 | // Extract and replace CSS variables in reverse order so that nested variables are replaced first
88 | cssVarPattern.findAll(css).toList().reversed().forEach { matchResult ->
89 | val varName = matchResult.groupValues[1].trim()
90 | val varValue = matchResult.groupValues[2].trim()
91 | newCss = newCss.replace("var(--$varName)", varValue)
92 | }
93 | return newCss
94 | }
95 |
96 | private fun String.baseName() = substringBefore(":").substringBefore(" ")
--------------------------------------------------------------------------------
/parsing/src/commonMain/kotlin/io/github/opletter/css2kobweb/ParseResults.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb
2 |
3 | sealed interface CssParseResult {
4 | fun asCodeBlocks(indentLevel: Int = 0): List
5 | }
6 |
7 | class ParsedStyleBlock(val properties: List, val label: String = "") : CssParseResult {
8 | // this purposely excludes [label] as that is just metadata used by the code itself
9 | override fun asCodeBlocks(indentLevel: Int): List {
10 | val indents = "\t".repeat(indentLevel)
11 | val coloredModifiers = properties.flatMap {
12 | listOf(CodeBlock("\n\t$indents.", CodeElement.Plain)) +
13 | it.asCodeBlocks(indentLevel + 1, functionType = CodeElement.ExtensionFun)
14 | }
15 | return listOf(CodeBlock("${indents}Modifier", CodeElement.Plain)) + coloredModifiers
16 | }
17 |
18 | override fun toString(): String = asCodeBlocks().joinToString("") { it.text }
19 | }
20 |
21 | class ParsedKeyframes(private val name: String, val modifiers: List) : CssParseResult {
22 | override fun asCodeBlocks(indentLevel: Int): List {
23 | return buildList {
24 | add(CodeBlock("val ", CodeElement.Keyword))
25 | add(CodeBlock(kebabToPascalCase(name), CodeElement.Property))
26 | add(CodeBlock(" = Keyframes {\n", CodeElement.Plain))
27 | modifiers.forEach { block ->
28 | add(CodeBlock("\t", CodeElement.Plain))
29 |
30 | val labelParts = block.label.split(',').mapNotNull { Arg.UnitNum.ofOrNull(it.trim()) }
31 | if (block.label == "from" || block.label == "to") {
32 | add(CodeBlock(block.label, CodeElement.Plain))
33 | } else if (labelParts.size == 1) {
34 | addAll(labelParts.single().asCodeBlocks(1))
35 | } else {
36 | addAll(Arg.Function("each", labelParts).asCodeBlocks(1))
37 | }
38 |
39 | add(CodeBlock(" {\n", CodeElement.Plain))
40 | addAll(block.asCodeBlocks(2))
41 | add(CodeBlock("\n\t}\n", CodeElement.Plain))
42 | }
43 | add(CodeBlock("}\n", CodeElement.Plain))
44 | }
45 | }
46 |
47 | override fun toString(): String = asCodeBlocks().joinToString("") { it.text }
48 | }
49 |
50 | class ParsedCssStyles(private val styles: List) : CssParseResult {
51 | override fun asCodeBlocks(indentLevel: Int): List {
52 | val globalModifierCode = styles.flatMap { style ->
53 | style.modifiers.values.flatMap { it.filterModifiers() }
54 | }.distinctBy { it.value }.flatMap { modifier ->
55 | listOf(
56 | CodeBlock("private val ", CodeElement.Keyword),
57 | CodeBlock("${modifier.value} = ", CodeElement.Plain),
58 | ) + modifier.modifier.asCodeBlocks() + CodeBlock("\n", CodeElement.Plain)
59 | }
60 | val stylesCode = styles.flatMap { it.asCodeBlocks() }
61 | return globalModifierCode + stylesCode
62 | }
63 |
64 | override fun toString(): String = asCodeBlocks().joinToString("") { it.text }
65 | }
66 |
67 | class ParsedBlocks(
68 | private val styles: ParsedCssStyles,
69 | private val keyframes: List,
70 | ) : CssParseResult {
71 | override fun asCodeBlocks(indentLevel: Int): List {
72 | return styles.asCodeBlocks() + keyframes.flatMap { it.asCodeBlocks() }
73 | }
74 |
75 | override fun toString(): String = asCodeBlocks().joinToString("") { it.text }
76 | }
77 |
78 | // convenient to reuse the same type for both
79 | typealias ParsedProperty = Arg.Function
80 |
81 | class ParsedCssStyle(private val name: String, val modifiers: Map) {
82 | fun asCodeBlocks(): List {
83 | val onlyBaseStyle = modifiers.size == 1 && modifiers.keys.first() == "base"
84 |
85 | val styleText = listOfNotNull(
86 | CodeBlock("val ", CodeElement.Keyword),
87 | CodeBlock("${name}Style", CodeElement.Property),
88 | CodeBlock(" = CssStyle${if (onlyBaseStyle) "." else ""}", CodeElement.Plain),
89 | if (onlyBaseStyle) CodeBlock("base", CodeElement.ExtensionFun) else null,
90 | CodeBlock(" {\n", CodeElement.Plain)
91 | )
92 |
93 | val localModifierCode = modifiers.values
94 | .flatMap { it.filterModifiers() }
95 | .distinctBy { it.value }
96 | .flatMap { modifier ->
97 | val modifierText = modifier.modifier.asCodeBlocks(indentLevel = 1)
98 | .let { listOf(CodeBlock("Modifier", CodeElement.Plain)) + it.drop(1) }
99 | val selectorText = listOf(
100 | CodeBlock("\tval ", CodeElement.Keyword),
101 | CodeBlock("${modifier.value} = ", CodeElement.Plain),
102 | )
103 | selectorText + modifierText + CodeBlock("\n", CodeElement.Plain)
104 | }
105 |
106 | val modifierText = if (onlyBaseStyle) {
107 | modifiers["base"]!!.asCodeBlocks(indentLevel = 1) + CodeBlock("\n", CodeElement.Plain)
108 | } else {
109 | modifiers.flatMap { (selectorName, modifier) ->
110 | val selector = if (selectorName.startsWith("cssRule(")) {
111 | listOf(
112 | CodeBlock("\tcssRule(", CodeElement.Plain),
113 | CodeBlock(parenContents(selectorName), CodeElement.String),
114 | CodeBlock(") {\n", CodeElement.Plain),
115 | )
116 | } else listOf(CodeBlock("\t$selectorName {\n", CodeElement.Plain))
117 |
118 | selector + modifier.asCodeBlocks(indentLevel = 2) + CodeBlock("\n\t}\n", CodeElement.Plain)
119 | }
120 | }
121 | return styleText + localModifierCode + modifierText + CodeBlock("}\n", CodeElement.Plain)
122 | }
123 |
124 | override fun toString(): String = asCodeBlocks().joinToString("") { it.text }
125 | }
--------------------------------------------------------------------------------
/parsing/src/commonMain/kotlin/io/github/opletter/css2kobweb/PostProcessing.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb
2 |
3 | import io.github.opletter.css2kobweb.constants.intoShorthandLambdaProperty
4 | import io.github.opletter.css2kobweb.functions.position
5 | import io.github.opletter.css2kobweb.functions.transition
6 |
7 | internal fun List>.postProcessProperties(): List {
8 | return map {
9 | val newProp = it.second.intoShorthandLambdaProperty()
10 | newProp.name to newProp
11 | }
12 | .combineLambdaModifiers()
13 | .replaceKeysIfEqual(setOf("width", "height"), "size")
14 | .replaceKeysIfEqual(setOf("minWidth", "minHeight"), "minSize")
15 | .replaceKeysIfEqual(setOf("maxWidth", "maxHeight"), "maxSize")
16 | .combineDirectionalModifiers("margin")
17 | .combineDirectionalModifiers("padding")
18 | .combineTransitionModifiers()
19 | .combineBackgroundPosition() // must be before combineBackgroundModifiers
20 | .combineBackgroundModifiers()
21 | .combineAnimationModifiers()
22 | .values.map { property ->
23 | val matchedFunction = setOf("width", "height", "size").find { it == property.name }
24 | if (matchedFunction != null && property.args.singleOrNull() == Arg.UnitNum.of("100%"))
25 | ParsedProperty("fillMax${matchedFunction.replaceFirstChar(Char::uppercase)}")
26 | else property
27 | }
28 | }
29 |
30 | private fun List>.combineLambdaModifiers(): Map {
31 | return groupBy({ it.first }, { it.second }).mapValues { (name, properties) ->
32 | if (properties.all { it.args.isEmpty() }) {
33 | Arg.Function(name, lambdaStatements = properties.flatMap { it.lambdaStatements })
34 | } else { // should only be one but if not just take last
35 | properties.last()
36 | }
37 | }
38 | }
39 |
40 | private fun Map.combineBackgroundPosition(): Map {
41 | // necessary for the combined x & y value to be valid, since the 3-value syntax is limited
42 | fun String.toTwoValue(direction: String) =
43 | if (Arg.UnitNum.ofOrNull(this) == null) this else "$direction $this"
44 |
45 | val posKey = "backgroundPosition"
46 |
47 | val x = this["${posKey}X"]?.args?.single()?.toString()?.splitNotInParens(',').orEmpty()
48 | val y = this["${posKey}Y"]?.args?.single()?.toString()?.splitNotInParens(',').orEmpty()
49 |
50 | if (x.isEmpty() && y.isEmpty()) return this
51 |
52 | val backgroundPositions = if (x.size >= y.size) {
53 | x.mapIndexed { index, xValue ->
54 | val yValue = y.getOrNull(index)?.toTwoValue("top").orEmpty()
55 | Arg.Function.position(xValue.toTwoValue("left") + " " + yValue)
56 | }
57 | } else {
58 | y.mapIndexed { index, yValue ->
59 | // technically if the x value isn't provided, the browser is supposed to figure it out,
60 | // but kobweb requires it, so we just use a dummy value
61 | val xValue = (x.getOrNull(index) ?: x.lastOrNull() ?: "left 50%").toTwoValue("left")
62 | Arg.Function.position("$xValue ${yValue.toTwoValue("top")}")
63 | }
64 | }.map { Arg.Function("BackgroundPosition.of", it) }
65 |
66 | val newProperty = ParsedProperty(posKey, backgroundPositions)
67 | return (this - "${posKey}X" - "${posKey}Y").plus(newProperty.name to newProperty)
68 | }
69 |
70 |
71 | private fun Map.combineBackgroundModifiers(): Map {
72 | fun String.getArgName() = this.substringAfter("background").substringBefore("Mode").lowercase()
73 |
74 | val existingBackground = this["background"]
75 | val existingBackgroundArgs = existingBackground?.args
76 | ?.dropWhile { !it.toString().startsWith("CSS") } // filter color arg
77 | ?.ifEmpty { null }
78 | // Un-reverse the initial parsing back to line up with order of other properties,
79 | // after which we will reverse again to match the order in kobweb.
80 | // Note that it's important to do parsing in declaration order, as if "background" specifies two images
81 | // but "backgroundImage" only specifies one, we want to use the latter's value for the first image
82 | ?.reversed()
83 |
84 | val propertyKeys = setOf(
85 | "backgroundImage", "backgroundRepeat", "backgroundSize", "backgroundPosition",
86 | "backgroundBlendMode", "backgroundOrigin", "backgroundClip", "backgroundAttachment"
87 | )
88 | val argNames = propertyKeys.map { it.getArgName() }
89 |
90 | val propertyValues = propertyKeys.mapNotNull { prop ->
91 | this[prop]?.let { prop to it.args }
92 | }.toMap()
93 |
94 | if (propertyValues.isEmpty() || (existingBackgroundArgs == null && propertyValues.values.all { it.size == 1 }))
95 | return this
96 |
97 | // According to the CSS spec, the number of layers is determined by the # of background-image values,
98 | // which can come from either the "backgroundImage" or "background"
99 | val layerIndices = propertyValues["backgroundImage"]?.indices ?: existingBackgroundArgs?.indices
100 | val backgroundProperties = layerIndices?.map { index ->
101 | val args = propertyValues.mapNotNull { (prop, args) ->
102 | val originalArg = args.getOrNull(index) ?: return@mapNotNull null
103 | val adjustedArg = if (prop == "backgroundImage" && originalArg is Arg.Function) {
104 | if (originalArg.name == "url")
105 | Arg.Function("BackgroundImage.of", originalArg)
106 | else Arg.ExtensionCall(originalArg, Arg.Function("toImage"))
107 | } else originalArg
108 |
109 | Arg.NamedArg(prop.getArgName(), adjustedArg)
110 | }
111 | val existingArgs = (existingBackgroundArgs?.get(index) as Arg.Function?)?.args.orEmpty()
112 | val combinedArgs = (existingArgs + args).sortedBy { argNames.indexOf(it.toString().substringBefore(" ")) }
113 |
114 | Arg.Function("Background.of", combinedArgs)
115 | }.orEmpty().reversed() // args order reversed as in kobweb
116 |
117 | val color = this["backgroundColor"]?.args
118 | ?: listOfNotNull(existingBackground?.args?.firstOrNull().takeIf { !it.toString().startsWith("CSS") })
119 |
120 | val newProperty = ParsedProperty("background", color + backgroundProperties)
121 |
122 | return (this - propertyKeys - "backgroundColor") + (newProperty.name to newProperty)
123 | }
124 |
125 | private fun Map.combineAnimationModifiers(): Map {
126 | fun String.getArgName() = this.substringAfter("animation").replaceFirstChar { it.lowercase() }
127 |
128 | val existingAnimation = this["animation"]
129 | val existingAnimationArgs = existingAnimation?.args?.ifEmpty { null }
130 |
131 | val propertyKeys = setOf(
132 | "animationName",
133 | "animationDuration",
134 | "animationTimingFunction",
135 | "animationDelay",
136 | "animationIterationCount",
137 | "animationDirection",
138 | "animationFillMode",
139 | "animationPlayState",
140 | )
141 | val argNames = propertyKeys.map { it.getArgName() }
142 |
143 | val propertyValues = propertyKeys.mapNotNull { prop ->
144 | this[prop]?.let { prop to it.args }
145 | }.toMap()
146 |
147 | // kobweb currently only supports specifying the whole animation as a single property, so we have to handle
148 | // all cases where one of these properties is individually specified in css. If this changes,
149 | // then this return condition can be expanded to match the one for [background]
150 |
151 | // IMPORTANT: we currently take advantage of the above fact by using combineAnimationModifiers
152 | // as the sole place where `Animation.of(...)` gets transformed into 'Name.toAnimation(...)'
153 | // If the `animation-...` properties are ever supported individually, this would need to be accounted for
154 | if (propertyValues.isEmpty() && existingAnimation == null)
155 | return this
156 |
157 | val animationProperties = (propertyValues.values.firstOrNull()?.indices ?: (0..0)).map { index ->
158 | val args = propertyValues.map { (prop, args) ->
159 | Arg.NamedArg(prop.getArgName(), args[index])
160 | }
161 | val existingArgs = (existingAnimationArgs?.get(index) as Arg.Function?)?.args.orEmpty()
162 | val combinedArgs = (existingArgs + args).sortedBy { argNames.indexOf(it.toString().substringBefore(" ")) }
163 |
164 | val (name, otherArgs) = combinedArgs.partition { it is Arg.NamedArg && it.name == "name" }
165 |
166 | name.singleOrNull()?.let { arg ->
167 | check(arg is Arg.NamedArg)
168 | Arg.ExtensionCall(
169 | Arg.Property.fromKebabValue(null, arg.value.toString().removeSurrounding("\"")),
170 | Arg.Function("toAnimation", otherArgs)
171 | )
172 | } ?: Arg.Function("Animation.of", combinedArgs)
173 | }
174 | val newProperty = ParsedProperty("animation", animationProperties)
175 |
176 | return (this - propertyKeys).plus(newProperty.name to newProperty)
177 | }
178 |
179 | private fun Map.combineTransitionModifiers(): Map {
180 | val transitionProperties = this["transitionProperty"]?.args
181 | // treat "transition" as "transitionProperty" if all it contains are properties
182 | ?: this["transition"]?.args?.mapNotNull { (it as? Arg.Function)?.args?.singleOrNull() }
183 | ?: return this
184 |
185 | val propertyKeys = setOf(
186 | "transitionProperty",
187 | "transitionDuration",
188 | "transitionTimingFunction",
189 | "transitionDelay",
190 | )
191 | val propertyValues = propertyKeys.map { this[it]?.args }
192 |
193 | val transitionGroup = transitionProperties.size > 1 &&
194 | propertyValues.drop(1).all { it == null || it.size == 1 }
195 |
196 | val combinedProperties = if (transitionGroup) {
197 | Arg.Function.transition(
198 | property = Arg.Function("setOf", transitionProperties),
199 | duration = propertyValues.getOrNull(1)?.getOrNull(0),
200 | remainingArgs = propertyValues.drop(2).mapNotNull { it?.getOrNull(0) }
201 | ).let { listOf(it) }
202 | } else {
203 | val otherProperties = propertyValues.drop(1).filterNotNull()
204 | if (otherProperties.isEmpty() || otherProperties.any { it.size != transitionProperties.size })
205 | return this
206 |
207 | transitionProperties.indices.map { index ->
208 | Arg.Function.transition(
209 | property = transitionProperties[index],
210 | duration = propertyValues[1]?.getOrNull(index),
211 | remainingArgs = propertyValues.drop(2).mapNotNull { it?.getOrNull(index) }
212 | )
213 | }
214 | }
215 | return (this - propertyKeys) + ("transition" to ParsedProperty("transition", combinedProperties))
216 | }
217 |
218 | private fun Map.replaceKeysIfEqual(
219 | keysToReplace: Set,
220 | newKey: String,
221 | ): Map {
222 | val values = keysToReplace.mapNotNull { get(it)?.args }
223 | return if (values.size == keysToReplace.size && values.toSet().size == 1) {
224 | minus(keysToReplace) + (newKey to ParsedProperty(newKey, values.first()))
225 | } else this
226 | }
227 |
228 | private fun Map.combineDirectionalModifiers(property: String): Map {
229 | // consider whether this should be combined with the above replaceKeysIfEqual function
230 | fun MutableMap.replaceIfEqual(keysToReplace: Set, newKey: String) {
231 | val values = keysToReplace.mapNotNull { get(it)?.value }
232 | if (values.size == keysToReplace.size && values.toSet().size == 1) {
233 | keysToReplace.forEach { remove(it) }
234 | put(newKey, Arg.NamedArg(newKey, values.first()))
235 | }
236 | }
237 |
238 | val directions = setOf("Top", "Right", "Bottom", "Left") // capitalized for camelCase
239 | val processedArgs = buildMap {
240 | directions.forEach { direction ->
241 | this@combineDirectionalModifiers[property + direction]
242 | ?.let { put(direction.lowercase(), Arg.NamedArg(direction.lowercase(), it.args.single())) }
243 | }
244 | replaceIfEqual(setOf("left", "right"), "leftRight")
245 | if ("leftRight" in this || ("left" !in this && "right" !in this))
246 | replaceIfEqual(setOf("top", "bottom"), "topBottom")
247 | replaceIfEqual(setOf("leftRight", "topBottom"), "all")
248 | }.ifEmpty { return this }
249 |
250 | val finalArgs = listOfNotNull(
251 | processedArgs["top"],
252 | processedArgs["topBottom"],
253 | processedArgs["leftRight"],
254 | processedArgs["right"],
255 | processedArgs["bottom"],
256 | processedArgs["left"],
257 | processedArgs["all"]?.value, // ignore name for "all"
258 | )
259 |
260 | return minus(directions.map { property + it }.toSet()) + (property to ParsedProperty(property, finalArgs))
261 | }
--------------------------------------------------------------------------------
/parsing/src/commonMain/kotlin/io/github/opletter/css2kobweb/Properties.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb
2 |
3 | import io.github.opletter.css2kobweb.functions.*
4 | import kotlin.math.min
5 |
6 | private val GlobalValues = setOf("initial", "inherit", "unset", "revert")
7 |
8 | internal fun parseCssProperty(propertyName: String, value: String): ParsedProperty {
9 | if (propertyName == "transition") {
10 | val transitions = value.splitNotInParens(',').map { transition ->
11 | val params = transition.splitNotInParens(' ')
12 | .let { if (Arg.UnitNum.ofOrNull(it.first()) != null) listOf("all") + it else it }
13 | val thirdArg = params.getOrNull(2)?.let {
14 | Arg.UnitNum.ofOrNull(it) ?: parseCssProperty("transitionTimingFunction", it).args.singleOrNull()
15 | }
16 | val fourthArg = params.getOrNull(3)?.let { Arg.UnitNum.of(it) }
17 |
18 | Arg.Function.transition(
19 | property = Arg.Literal("\"${params[0]}\""),
20 | duration = params.getOrNull(1)?.let { Arg.UnitNum.of(it) },
21 | remainingArgs = listOfNotNull(thirdArg, fourthArg),
22 | )
23 | }
24 | return ParsedProperty(propertyName, transitions)
25 | }
26 | if (propertyName == "transform") {
27 | val statements = value.splitNotInParens(' ').map { func ->
28 | val args = parenContents(func).splitNotInParens(',').map {
29 | if (it.toDoubleOrNull() == 0.0 && (func.startsWith("matrix") || func.startsWith("scale")))
30 | Arg.RawNumber(0)
31 | else
32 | Arg.UnitNum.ofOrNull(it) ?: Arg.RawNumber(it.toIntOrNull() ?: it.toDouble())
33 | }
34 | Arg.Function(func.substringBefore('('), args)
35 | }
36 | return ParsedProperty(propertyName, lambdaStatements = statements)
37 | }
38 | if (propertyName == "aspectRatio" && '/' in value) {
39 | return ParsedProperty(
40 | propertyName,
41 | value.split('/').map { Arg.RawNumber(it.toIntOrNull() ?: it.toDouble()) }
42 | )
43 | }
44 | if (propertyName == "fontFamily") {
45 | return ParsedProperty(
46 | propertyName,
47 | value.splitNotInParens(',').map { Arg.Literal.withQuotesIfNecessary(it) }
48 | )
49 | }
50 | if (propertyName == "background") {
51 | return ParsedProperty(propertyName, parseBackground(value))
52 | }
53 | if (propertyName == "backgroundPosition" && value !in GlobalValues) {
54 | val args = value.splitNotInParens(',').map {
55 | if (it in GlobalValues) parseCssProperty(propertyName, it).args.single()
56 | else Arg.Function("BackgroundPosition.of", Arg.Function.position(it))
57 | }
58 | return ParsedProperty(propertyName, args)
59 | }
60 | if (propertyName == "backgroundPositionX" || propertyName == "backgroundPositionY") {
61 | // will be handled in postProcessing, preserve values for now
62 | return ParsedProperty(propertyName, Arg.Literal(value))
63 | }
64 | if (propertyName == "backgroundSize") {
65 | val args = value.splitNotInParens(',').map { subValue ->
66 | if (subValue in GlobalValues || subValue in setOf("cover", "contain")) {
67 | Arg.Property.fromKebabValue("BackgroundSize", subValue)
68 | } else {
69 | Arg.Function("BackgroundSize.of", subValue.splitNotInParens(' ').map { Arg.UnitNum.of(it) })
70 | }
71 | }
72 | return ParsedProperty(propertyName, args)
73 | }
74 | if (propertyName == "backgroundRepeat") {
75 | val args = value.splitNotInParens(',').map { subValue ->
76 | val values = subValue.splitNotInParens(' ')
77 | .map { Arg.Property.fromKebabValue(kebabToPascalCase(propertyName), it) }
78 |
79 | values.singleOrNull() ?: Arg.Function("BackgroundRepeat.of", values)
80 | }
81 | return ParsedProperty(propertyName, args)
82 | }
83 | if (propertyName == "animation") {
84 | return ParsedProperty(propertyName, parseAnimation(value))
85 | }
86 | if (propertyName == "animationName") {
87 | return ParsedProperty(propertyName, Arg.Literal("\"$value\""))
88 | }
89 | if (propertyName == "animationIterationCount") {
90 | val num = value.toIntOrNull() ?: value.toDoubleOrNull()
91 | val arg = if (num != null) {
92 | Arg.Function("AnimationIterationCount.of", Arg.RawNumber(num))
93 | } else {
94 | Arg.Property.fromKebabValue("AnimationIterationCount", value)
95 | }
96 | return ParsedProperty(propertyName, arg)
97 | }
98 | if (
99 | value !in GlobalValues && value != "none"
100 | && propertyName in setOf("gridAutoRows", "gridAutoColumns", "gridTemplateRows", "gridTemplateColumns")
101 | ) {
102 | return ParsedProperty(propertyName, lambdaStatements = parseGridRowCol(value))
103 | }
104 | if (value !in GlobalValues && propertyName == "flexFlow") {
105 | val subValues = value.splitNotInParens(' ')
106 | val indexOfWrap = subValues.indexOfFirst { "wrap" in it }
107 |
108 | return if (subValues.size == 2) {
109 | ParsedProperty(
110 | propertyName,
111 | parseCssProperty("flexDirection", subValues[1 - indexOfWrap]).args +
112 | parseCssProperty("flexWrap", subValues[indexOfWrap]).args
113 | )
114 | } else {
115 | val property = if (indexOfWrap != -1) "flexWrap" else "flexDirection"
116 | ParsedProperty(property, parseCssProperty(property, value).args)
117 | }
118 | }
119 | // kobweb treats "nowrap" as if it was "no-wrap", so we need to handle it separately
120 | if (propertyName == "whiteSpace" && value == "nowrap") {
121 | return ParsedProperty(propertyName, Arg.Property("WhiteSpace", "NoWrap"))
122 | }
123 |
124 | return value.splitNotBetween(setOf('(' to ')'), setOf(' ', ',', '/')).map { prop ->
125 | if (prop in GlobalValues) {
126 | return@map Arg.Property.fromKebabValue(classNamesFromProperty(propertyName), prop)
127 | }
128 |
129 | val unit = Arg.UnitNum.ofOrNull(prop)
130 | if (unit != null) {
131 | val takeRawZero = setOf(
132 | "zIndex", "opacity", "lineHeight", "flexGrow", "flexShrink", "flex", "order",
133 | "gridColumnEnd", "gridColumnStart", "gridRowEnd", "gridRowStart",
134 | )
135 |
136 | return@map if (unit.toString().substringBeforeLast('.') == "0" && propertyName in takeRawZero)
137 | Arg.RawNumber(0)
138 | else unit
139 | }
140 |
141 | val rawNum = prop.toIntOrNull() ?: prop.toDoubleOrNull()
142 | if (rawNum != null) {
143 | return@map Arg.RawNumber(rawNum)
144 | }
145 |
146 | Arg.asColorOrNull(prop)?.let { return@map it }
147 |
148 | if (prop.startsWith("linear-gradient(")) {
149 | return@map Arg.Function.linearGradient(parenContents(prop))
150 | }
151 | if (prop.startsWith("radial-gradient(")) {
152 | return@map Arg.Function.radialGradient(parenContents(prop))
153 | }
154 | if (prop.startsWith("conic-gradient(")) {
155 | return@map Arg.Function.conicGradient(parenContents(prop))
156 | }
157 |
158 | if (prop.startsWith("url(")) {
159 | val contents = parenContents(prop)
160 | return@map Arg.Function("url", Arg.Literal.withQuotesIfNecessary(contents))
161 | }
162 |
163 | if (prop.startsWith('"')) {
164 | return@map Arg.Literal(prop)
165 | }
166 | if (propertyName == "transitionProperty") {
167 | return@map Arg.Literal("\"$prop\"")
168 | }
169 |
170 | val className = classNamesFromProperty(propertyName)
171 |
172 | if (prop.endsWith(")")) {
173 | val functionPropertyName = if (propertyName.endsWith("TimingFunction") && prop.startsWith("steps(")) {
174 | "StepPosition"
175 | } else propertyName
176 |
177 | val filterFunctions = setOf(
178 | "blur", "brightness", "contrast", "dropShadow", "grayscale", "hueRotate", "invert", "saturate", "sepia",
179 | )
180 | val mathFunctions = setOf("clamp", "min", "max")
181 | val simpleGlobalFunctions = filterFunctions + mathFunctions
182 |
183 | val functionName = kebabToCamelCase(prop.substringBefore("("))
184 | val prefix = if (functionName in simpleGlobalFunctions) "" else "$className."
185 |
186 | val adjustedArgs = parenContents(prop).let { args ->
187 | // math function can contain expressions, so wrap them in calc() for parsing purposes
188 | if (functionName in mathFunctions)
189 | args.splitNotInParens(',').joinToString { "calc($it)" }
190 | else args
191 | }
192 |
193 | return@map Arg.Function("$prefix$functionName", parseCssProperty(functionPropertyName, adjustedArgs).args)
194 | }
195 |
196 | Arg.Property.fromKebabValue(className, prop).let {
197 | if (it.value == "Auto" && it.className != null && takesAutoLength(it.className)) {
198 | Arg.UnitNum.Auto
199 | } else it
200 | }
201 | }.let { ParsedProperty(propertyName, it) }
202 | }
203 |
204 | // Loose check for properties that often use "auto" as a length
205 | private fun takesAutoLength(className: String): Boolean {
206 | return className.startsWith("Padding") || className.startsWith("Margin")
207 | || className.endsWith("Width") || className.endsWith("Height")
208 | }
209 |
210 | private fun classNamesFromProperty(propertyName: String): String {
211 | return when (propertyName) {
212 | "display" -> "DisplayStyle"
213 | "overflowY", "overflowX" -> "Overflow"
214 | "float" -> "CSSFloat"
215 | "gridTemplateRows", "gridTemplateColumns" -> "GridTemplate"
216 | "gridAutoRows", "gridAutoColumns" -> "GridAuto"
217 | "border", "borderStyle", "borderTop", "borderBottom", "borderLeft", "borderRight",
218 | "borderTopStyle", "borderBottomStyle", "borderLeftStyle", "borderRightStyle",
219 | "outline", "outlineStyle",
220 | -> "LineStyle"
221 |
222 | else -> propertyName.replaceFirstChar { it.uppercase() }
223 | }
224 | }
225 |
226 | private fun parseBackground(value: String): List {
227 | // kobweb reverses order of backgrounds
228 | val backgrounds = value.splitNotInParens(',').reversed()
229 | .map { it.splitNotInParens('/').joinToString(" / ") }
230 |
231 | val backgroundObjects = backgrounds.map { background ->
232 | val repeatRegex = """(repeat-x|repeat-y|repeat|space|round|no-repeat)\b""".toRegex()
233 | val attachmentRegex = """(scroll|fixed|local)\b""".toRegex()
234 | val boxRegex = """(border-box|padding-box|content-box)\b""".toRegex()
235 |
236 | val backgroundArgs = buildList {
237 | val image = background.splitNotInParens(' ').firstOrNull {
238 | it.startsWith("url(") || it.startsWith("linear-gradient(")
239 | || it.startsWith("radial-gradient(") || it.startsWith("conic-gradient(")
240 | }
241 | if (image != null) {
242 | val imageArg = parseCssProperty("backgroundImage", image).args.single().let {
243 | if (it is Arg.Function) {
244 | if (it.name == "url") Arg.Function("BackgroundImage.of", it)
245 | else Arg.ExtensionCall(it, Arg.Function("toImage"))
246 | } else it
247 | }
248 | add(Arg.NamedArg("image", imageArg))
249 | }
250 |
251 | val repeat = repeatRegex.find(background)?.value
252 | if (repeat != null) {
253 | val repeatArg = parseCssProperty("backgroundRepeat", repeat).args.single()
254 | add(Arg.NamedArg("repeat", repeatArg))
255 | }
256 |
257 | val attachment = attachmentRegex.find(background)?.value
258 | if (attachment != null) {
259 | val attachmentArg = parseCssProperty("backgroundAttachment", attachment).args.single()
260 | add(Arg.NamedArg("attachment", attachmentArg))
261 | }
262 |
263 | val boxMatches = boxRegex.findAll(background).toList()
264 | if (boxMatches.isNotEmpty()) {
265 | val (origin, clip) = if (boxMatches.size == 2) boxMatches else List(2) { boxMatches.single() }
266 | val originArg = parseCssProperty("backgroundOrigin", origin.value).args.single()
267 | val clipArg = parseCssProperty("backgroundClip", clip.value).args.single()
268 | add(Arg.NamedArg("origin", originArg))
269 | add(Arg.NamedArg("clip", clipArg))
270 | }
271 |
272 | val otherProps = background.splitNotInParens(' ') - setOfNotNull(image, repeat, attachment) -
273 | boxMatches.map { it.value }.toSet()
274 |
275 | val slashIndex = otherProps.indexOf("/")
276 | if (slashIndex != -1) {
277 | val position = ((slashIndex - 4).coerceAtLeast(0).. {
309 | val animations = value.splitNotInParens(',')
310 | val animationObjects = animations.map { animation ->
311 | val timingRegex =
312 | """((ease-in-out|ease-in|ease-out|ease|linear|step-start|step-end)|cubic-bezier\([^)]*\)|steps\([^)]*\))""".toRegex()
313 | val directionRegex = """(normal|reverse|alternate|alternate-reverse)\b""".toRegex()
314 | val fillModeRegex = """(none|forwards|backwards|both)\b""".toRegex()
315 | val playStateRegex = """(running|paused)\b""".toRegex()
316 |
317 | val parts = animation.splitNotInParens(' ')
318 |
319 | val units = parts.mapNotNull { Arg.UnitNum.ofOrNull(it) }
320 |
321 | val animationArgs = buildList {
322 | if (units.isNotEmpty()) {
323 | add(Arg.NamedArg("duration", units.first()))
324 | }
325 |
326 | val timing = timingRegex.find(animation)?.value
327 | if (timing != null) {
328 | val repeatArg = parseCssProperty("animationTimingFunction", timing).args.single()
329 | add(Arg.NamedArg("timingFunction", repeatArg))
330 | }
331 |
332 | if (units.size > 1) {
333 | add(Arg.NamedArg("delay", units[1]))
334 | }
335 |
336 | val iterationCount = parts.firstNotNullOfOrNull { it.toIntOrNull() ?: it.toDoubleOrNull() }
337 | ?: "infinite".takeIf { it in parts }
338 |
339 | if (iterationCount != null) {
340 | val iterationCountArg =
341 | parseCssProperty("animationIterationCount", iterationCount.toString()).args.single()
342 | add(Arg.NamedArg("iterationCount", iterationCountArg))
343 | }
344 |
345 | val direction = directionRegex.find(animation)?.value
346 | if (direction != null) {
347 | val directionArg = parseCssProperty("animationDirection", direction).args.single()
348 | add(Arg.NamedArg("direction", directionArg))
349 | }
350 |
351 | val fillMode = fillModeRegex.find(animation)?.value
352 | if (fillMode != null) {
353 | val fillModeArg = parseCssProperty("animationFillMode", fillMode).args.single()
354 | add(Arg.NamedArg("fillMode", fillModeArg))
355 | }
356 |
357 | val playState = playStateRegex.find(animation)?.value
358 | if (playState != null) {
359 | val playStateArg = parseCssProperty("animationPlayState", playState).args.single()
360 | add(Arg.NamedArg("playState", playStateArg))
361 | }
362 |
363 | val otherProps = parts.filter { Arg.UnitNum.ofOrNull(it) == null } -
364 | setOfNotNull(timing, iterationCount.toString(), direction, fillMode, playState)
365 |
366 | val name = otherProps.lastOrNull { it.isNotBlank() } // search from end per css best practice
367 | if (name != null) {
368 | add(0, Arg.NamedArg("name", parseCssProperty("animationName", name).args.single()))
369 | }
370 | }
371 | Arg.Function("Animation.of", animationArgs)
372 | }.filter { it.args.isNotEmpty() }
373 |
374 | return animationObjects
375 | }
376 |
377 | private fun parseGridRowCol(value: String): List {
378 | return value.splitNotBetween(setOf('(' to ')', '[' to ']'), setOf(' ')).map { subValue ->
379 | if (subValue.startsWith("[")) {
380 | Arg.Function(
381 | "lineNames",
382 | subValue.drop(1).dropLast(1).splitNotInParens(' ').map { Arg.Literal.withQuotesIfNecessary(it) }
383 | )
384 | } else if (subValue.startsWith("minmax") || subValue.startsWith("fit-content")) {
385 | Arg.Function(
386 | kebabToCamelCase(subValue.substringBefore("(")),
387 | parenContents(subValue).splitNotInParens(',').map {
388 | Arg.UnitNum.ofOrNull(it.trim()) ?: Arg.Property(null, kebabToCamelCase(it.trim()))
389 | }
390 | )
391 | } else if (subValue.startsWith("repeat")) {
392 | val repeatArgs = parenContents(subValue).splitNotInParens(',')
393 | val repeatCount = repeatArgs[0].toIntOrNull()?.let { Arg.RawNumber(it) }
394 | ?: Arg.Property(null, kebabToCamelCase(repeatArgs[0]))
395 | Arg.Function("repeat", listOf(repeatCount), parseGridRowCol(repeatArgs[1]))
396 | } else {
397 | Arg.Function(
398 | "size",
399 | Arg.UnitNum.ofOrNull(subValue) ?: Arg.Property(null, kebabToCamelCase(subValue))
400 | )
401 | }
402 | }
403 | }
--------------------------------------------------------------------------------
/parsing/src/commonMain/kotlin/io/github/opletter/css2kobweb/StringUtils.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb
2 |
3 | internal fun kebabToPascalCase(str: String): String {
4 | return str.split('-').joinToString("") { part ->
5 | part.replaceFirstChar { it.titlecase() }
6 | }
7 | }
8 |
9 | internal fun kebabToCamelCase(str: String): String = kebabToPascalCase(str).replaceFirstChar { it.lowercase() }
10 |
11 | internal fun parenContents(str: String): String = str.substringAfter('(').substringBeforeLast(')').trim()
12 |
13 | /**
14 | * Splits a string based on delimiters, except those present between sets of [groups] chars,
15 | * and except those between quotes.
16 | *
17 | * @param groups a set of char pairs, where the string will not be split between the first and second char of each pair
18 | */
19 | internal fun String.splitNotBetween(
20 | groups: Set>,
21 | splitOn: Set,
22 | ): List = splitNotBetween(groups, splitOn, ParseState())
23 |
24 | private data class ParseState(
25 | val quotesCount: Int = 0,
26 | val groupCounts: Map = emptyMap(),
27 | val buffer: String = "",
28 | val result: List = emptyList(),
29 | )
30 |
31 | /**
32 | * Splits a string based on delimiters, except those present between sets of [groups] chars,
33 | * and except those between quotes.
34 | *
35 | * @param groups a set of char pairs, where the string will not be split between the first and second char of each pair
36 | */
37 | private tailrec fun String.splitNotBetween(
38 | groups: Set>,
39 | splitOn: Set,
40 | state: ParseState,
41 | ): List {
42 | if (isEmpty()) {
43 | return (state.result + state.buffer).filter { it.isNotBlank() }
44 | }
45 | val nextState = when (val ch = first()) {
46 | in groups.flatMap { listOf(it.first, it.second) } -> {
47 | val openChar = groups.first { ch == it.first || ch == it.second }.first
48 | val isGroupStart = openChar == ch
49 | val newGroupCount = (state.groupCounts[openChar] ?: 0) + if (isGroupStart) 1 else -1
50 | val restartBuffer = isGroupStart && newGroupCount == 1 && splitOn.contains(ch)
51 |
52 | state.copy(
53 | buffer = if (restartBuffer) "" else state.buffer + ch,
54 | groupCounts = state.groupCounts + (openChar to newGroupCount),
55 | result = if (restartBuffer) state.result + state.buffer else state.result
56 | )
57 | }
58 |
59 | '"' -> state.copy(buffer = state.buffer + ch, quotesCount = state.quotesCount + 1)
60 | in splitOn -> {
61 | if (state.groupCounts.values.any { it > 0 } || state.quotesCount % 2 == 1) {
62 | state.copy(buffer = state.buffer + ch)
63 | } else {
64 | state.copy(buffer = "", result = state.result + state.buffer)
65 | }
66 | }
67 |
68 | else -> state.copy(buffer = state.buffer + ch)
69 | }
70 | return drop(1).splitNotBetween(groups, splitOn, nextState)
71 | }
72 |
73 | internal fun String.splitNotInParens(split: Char): List {
74 | return splitNotBetween(setOf('(' to ')'), splitOn = setOf(split))
75 | .map { it.trim() }.filter { it.isNotEmpty() }
76 | }
--------------------------------------------------------------------------------
/parsing/src/commonMain/kotlin/io/github/opletter/css2kobweb/StyleModifier.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb
2 |
3 | /** Represents the different ways a modifier can be used inside a CssStyle */
4 | sealed class StyleModifier(val value: String) {
5 | sealed class Normal(value: String) : StyleModifier(value)
6 |
7 | class Inline(val parsedModifier: ParsedStyleBlock) : Normal(parsedModifier.toString())
8 | class Global(key: String, val modifier: ParsedStyleBlock) : Normal(key)
9 | class Local(key: String, val modifier: ParsedStyleBlock) : Normal(key)
10 |
11 | class Composite(val modifiers: List) : StyleModifier(run {
12 | val (inlineModifiers, sharedModifiers) = modifiers.partition { it is Inline }
13 | val start = sharedModifiers.firstOrNull()?.let { first ->
14 | first.toString() + sharedModifiers.drop(1).joinToString("") { "\n\t.then($it)" }
15 | } ?: "Modifier"
16 | val end = inlineModifiers.joinToString("") { "\n" + it.toString().substringAfter("\n") }
17 |
18 | start + end
19 | })
20 |
21 | override fun toString(): String = value
22 |
23 | operator fun plus(other: Normal): Composite {
24 | return when (this) {
25 | is Composite -> Composite(modifiers + other)
26 | is Normal -> Composite(listOf(this, other))
27 | }
28 | }
29 |
30 | fun asCodeBlocks(indentLevel: Int = 0): List {
31 | val indents = "\t".repeat(indentLevel)
32 | return when (this) {
33 | is Global, is Local -> listOf(CodeBlock(indents + value, CodeElement.Plain))
34 | is Inline -> parsedModifier.asCodeBlocks(indentLevel)
35 | is Composite -> {
36 | val (inlineModifiers, sharedModifiers) = modifiers.partition { it is Inline }
37 | val start = sharedModifiers.firstOrNull()?.let { style ->
38 | indents + style.toString() +
39 | sharedModifiers.drop(1).joinToString("") { "\n\t$indents.then($it)" }
40 | } ?: "${indents}Modifier"
41 | val end = inlineModifiers.flatMap { style ->
42 | style.asCodeBlocks(indentLevel).let { if (style is Inline) it.drop(1) else it }
43 | }
44 | listOf(CodeBlock(start, CodeElement.Plain)) + end
45 | }
46 | }
47 | }
48 | }
49 |
50 | inline fun StyleModifier.filterModifiers(): List {
51 | return when (this) {
52 | is StyleModifier.Composite -> modifiers.filterIsInstance()
53 | is T -> listOf(this)
54 | else -> emptyList()
55 | }
56 | }
--------------------------------------------------------------------------------
/parsing/src/commonMain/kotlin/io/github/opletter/css2kobweb/constants/Colors.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb.constants
2 |
3 | internal val colors = listOf(
4 | "Transparent",
5 | "AliceBlue",
6 | "AntiqueWhite",
7 | "Aqua",
8 | "Aquamarine",
9 | "Azure",
10 | "Beige",
11 | "Bisque",
12 | "Black",
13 | "BlanchedAlmond",
14 | "Blue",
15 | "BlueViolet",
16 | "Brown",
17 | "BurlyWood",
18 | "CadetBlue",
19 | "Chartreuse",
20 | "Chocolate",
21 | "Coral",
22 | "CornflowerBlue",
23 | "Cornsilk",
24 | "Crimson",
25 | "Cyan",
26 | "DarkBlue",
27 | "DarkCyan",
28 | "DarkGoldenRod",
29 | "DarkGray",
30 | "DarkGrey",
31 | "DarkGreen",
32 | "DarkKhaki",
33 | "DarkMagenta",
34 | "DarkOliveGreen",
35 | "DarkOrange",
36 | "DarkOrchid",
37 | "DarkRed",
38 | "DarkSalmon",
39 | "DarkSeaGreen",
40 | "DarkSlateBlue",
41 | "DarkSlateGray",
42 | "DarkSlateGrey",
43 | "DarkTurquoise",
44 | "DarkViolet",
45 | "DeepPink",
46 | "DeepSkyBlue",
47 | "DimGray",
48 | "DimGrey",
49 | "DodgerBlue",
50 | "FireBrick",
51 | "FloralWhite",
52 | "ForestGreen",
53 | "Fuchsia",
54 | "Gainsboro",
55 | "GhostWhite",
56 | "Gold",
57 | "GoldenRod",
58 | "Gray",
59 | "Grey",
60 | "Green",
61 | "GreenYellow",
62 | "HoneyDew",
63 | "HotPink",
64 | "IndianRed",
65 | "Indigo",
66 | "Ivory",
67 | "Khaki",
68 | "Lavender",
69 | "LavenderBlush",
70 | "LawnGreen",
71 | "LemonChiffon",
72 | "LightBlue",
73 | "LightCoral",
74 | "LightCyan",
75 | "LightGoldenRodYellow",
76 | "LightGray",
77 | "LightGrey",
78 | "LightGreen",
79 | "LightPink",
80 | "LightSalmon",
81 | "LightSeaGreen",
82 | "LightSkyBlue",
83 | "LightSlateGray",
84 | "LightSlateGrey",
85 | "LightSteelBlue",
86 | "LightYellow",
87 | "Lime",
88 | "LimeGreen",
89 | "Linen",
90 | "Magenta",
91 | "Maroon",
92 | "MediumAquaMarine",
93 | "MediumBlue",
94 | "MediumOrchid",
95 | "MediumPurple",
96 | "MediumSeaGreen",
97 | "MediumSlateBlue",
98 | "MediumSpringGreen",
99 | "MediumTurquoise",
100 | "MediumVioletRed",
101 | "MidnightBlue",
102 | "MintCream",
103 | "MistyRose",
104 | "Moccasin",
105 | "NavajoWhite",
106 | "Navy",
107 | "OldLace",
108 | "Olive",
109 | "OliveDrab",
110 | "Orange",
111 | "OrangeRed",
112 | "Orchid",
113 | "PaleGoldenRod",
114 | "PaleGreen",
115 | "PaleTurquoise",
116 | "PaleVioletRed",
117 | "PapayaWhip",
118 | "PeachPuff",
119 | "Peru",
120 | "Pink",
121 | "Plum",
122 | "PowderBlue",
123 | "Purple",
124 | "RebeccaPurple",
125 | "Red",
126 | "RosyBrown",
127 | "RoyalBlue",
128 | "SaddleBrown",
129 | "Salmon",
130 | "SandyBrown",
131 | "SeaGreen",
132 | "SeaShell",
133 | "Sienna",
134 | "Silver",
135 | "SkyBlue",
136 | "SlateBlue",
137 | "SlateGray",
138 | "SlateGrey",
139 | "Snow",
140 | "SpringGreen",
141 | "SteelBlue",
142 | "Tan",
143 | "Teal",
144 | "Thistle",
145 | "Tomato",
146 | "Turquoise",
147 | "Violet",
148 | "Wheat",
149 | "White",
150 | "WhiteSmoke",
151 | "Yellow",
152 | "YellowGreen"
153 | )
--------------------------------------------------------------------------------
/parsing/src/commonMain/kotlin/io/github/opletter/css2kobweb/constants/CssRules.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb.constants
2 |
3 | internal val cssRules = mapOf(
4 | ":any-link" to "anyLink",
5 | ":link" to "link",
6 | ":target" to "target",
7 | ":visited" to "visited",
8 | ":hover" to "hover",
9 | ":active" to "active",
10 | ":focus" to "focus",
11 | ":focus-visible" to "focusVisible",
12 | ":focus-within" to "focusWithin",
13 | ":autofill" to "autofill",
14 | ":enabled" to "enabled",
15 | ":disabled" to "disabled",
16 | ":read-only" to "readOnly",
17 | ":read-write" to "readWrite",
18 | ":placeholder-shown" to "placeholderShown",
19 | ":default" to "default",
20 | ":checked" to "checked",
21 | ":indeterminate" to "indeterminate",
22 | ":valid" to "valid",
23 | ":invalid" to "invalid",
24 | ":in-range" to "inRange",
25 | ":out-of-range" to "outOfRange",
26 | ":required" to "required",
27 | ":optional" to "optional",
28 | ":user-valid" to "userValid",
29 | ":user-invalid" to "userInvalid",
30 | ":root" to "root",
31 | ":empty" to "empty",
32 | ":first-child" to "firstChild",
33 | ":last-child" to "lastChild",
34 | ":only-child" to "onlyChild",
35 | ":first-of-type" to "firstOfType",
36 | ":last-of-type" to "lastOfType",
37 | ":only-of-type" to "onlyOfType",
38 | // support both single and double colon
39 | "::before" to "before",
40 | ":before" to "before",
41 | "::after" to "after",
42 | ":after" to "after",
43 | "::selection" to "selection",
44 | ":selection" to "selection",
45 | "::first-letter" to "firstLetter",
46 | ":first-letter" to "firstLetter",
47 | "::first-line" to "firstLine",
48 | ":first-line" to "firstLine",
49 | "::placeholder" to "placeholder",
50 | ":placeholder" to "placeholder",
51 | )
--------------------------------------------------------------------------------
/parsing/src/commonMain/kotlin/io/github/opletter/css2kobweb/constants/ShorthandProperties.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb.constants
2 |
3 | import io.github.opletter.css2kobweb.Arg
4 | import io.github.opletter.css2kobweb.ParsedProperty
5 |
6 | private class ShorthandProperty(val property: String, val subProperties: List)
7 |
8 | // TODO: In the future we should have an option for "strict" matching the original CSS (using the scope functions)
9 | // vs. a mode that uses named args instead. Note that even in strict mode, if all shorthands are specified, we
10 | // should probably still use named args if available.
11 | private val shorthandProperties = listOf(
12 | ShorthandProperty("border", listOf("Width", "Style", "Color")),
13 | ShorthandProperty("borderTop", listOf("Width", "Style", "Color")),
14 | ShorthandProperty("borderBottom", listOf("Width", "Style", "Color")),
15 | ShorthandProperty("borderRight", listOf("Width", "Style", "Color")),
16 | ShorthandProperty("borderLeft", listOf("Width", "Style", "Color")),
17 | ShorthandProperty("overflow", listOf("X", "Y")),
18 | ShorthandProperty("paddingInline", listOf("Start", "End")),
19 | ShorthandProperty("paddingBlock", listOf("Start", "End")),
20 | // Currently don't use scope for these as it's usually unnecessary, and instead we can provide smart reduced
21 | // named properties (like turning equal "top" and "bottom" into "topBottom").
22 | // These should be re-enabled after the to-do above is addressed.
23 | // ShorthandProperty("padding", listOf("Top", "Right", "Bottom", "Left")),
24 | // ShorthandProperty("margin", listOf("Top", "Right", "Bottom", "Left")),
25 | ShorthandProperty("font", listOf("Alternates", "Caps", "EastAsian", "Emoji", "Ligatures", "Numeric", "Settings")),
26 | ).flatMap { shortHand ->
27 | shortHand.subProperties.map { "${shortHand.property}$it" to it }
28 | }.toMap()
29 |
30 | fun ParsedProperty.intoShorthandLambdaProperty(): ParsedProperty {
31 | return shorthandProperties[name]?.let { shorthandFun ->
32 | ParsedProperty(
33 | name.substringBefore(shorthandFun),
34 | lambdaStatements = listOf(
35 | Arg.Function(shorthandFun.replaceFirstChar { it.lowercase() }, args, lambdaStatements)
36 | )
37 | )
38 | } ?: this
39 | }
--------------------------------------------------------------------------------
/parsing/src/commonMain/kotlin/io/github/opletter/css2kobweb/constants/Units.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb.constants
2 |
3 | internal val units = mapOf("%" to "percent", "rem" to "cssRem") + listOf(
4 | // "%",
5 | "em",
6 | "ex",
7 | "ch",
8 | "ic",
9 | // "rem",
10 | "lh",
11 | "rlh",
12 | "vw",
13 | "vh",
14 | "vi",
15 | "vb",
16 | "vmin",
17 | "vmax",
18 | "svb",
19 | "svh",
20 | "svi",
21 | "svmax",
22 | "svmin",
23 | "svw",
24 | "lvb",
25 | "lvh",
26 | "lvi",
27 | "lvmax",
28 | "lvmin",
29 | "lvw",
30 | "dvb",
31 | "dvh",
32 | "dvi",
33 | "dvmax",
34 | "dvmin",
35 | "dvw",
36 | "vi",
37 | "vb",
38 | "cm",
39 | "mm",
40 | "Q",
41 | "pt",
42 | "pc",
43 | "px",
44 | "deg",
45 | "grad",
46 | "rad",
47 | "turn",
48 | "s",
49 | "ms",
50 | "Hz",
51 | "kHz",
52 | "dpi",
53 | "dpcm",
54 | "dppx",
55 | "fr",
56 | "number",
57 | ).associateWith { it }
--------------------------------------------------------------------------------
/parsing/src/commonMain/kotlin/io/github/opletter/css2kobweb/functions/Color.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb.functions
2 |
3 | import io.github.opletter.css2kobweb.Arg
4 | import io.github.opletter.css2kobweb.constants.colors
5 | import io.github.opletter.css2kobweb.parenContents
6 | import kotlin.math.roundToInt
7 |
8 | internal fun Arg.Companion.asColorOrNull(value: String): Arg? {
9 | if (value.startsWith("#") && ' ' !in value.trim()) {
10 | val hexValue = if (value.length <= 5) {
11 | value.drop(1).toList().joinToString("") { "$it$it" } // #RGBA = #RRGGBBAA = 0xRRGGBBAA
12 | } else {
13 | value.drop(1)
14 | }
15 | val function = if (hexValue.length == 8) "rgba" else "rgb"
16 | return Arg.Function("Color.$function", Arg.Hex(hexValue))
17 | }
18 | val color = colors.firstOrNull { it.lowercase() == value }
19 | if (color != null) {
20 | return Arg.Property("Colors", color)
21 | }
22 | if (value.startsWith("rgb") && value.endsWith(")")) {
23 | return rgbOrNull(value)
24 | }
25 | if (value.startsWith("hsl") && value.endsWith(")")) {
26 | return hslOrNull(value)
27 | }
28 | return null
29 | }
30 |
31 | private fun rgbOrNull(prop: String): Arg.Function? {
32 | val nums = parenContents(prop).split(' ', ',', '/').filter { it.isNotBlank() }
33 |
34 | val params = nums.take(3).map {
35 | if (it.endsWith("%")) Arg.Float(it.dropLast(1).toFloat() / 100)
36 | else Arg.RawNumber(it.toDouble().roundToInt())
37 | }
38 | if (nums.size == 3) {
39 | return Arg.Function("Color.rgb", params)
40 | }
41 | if (nums.size == 4) {
42 | val alpha = nums.last().let {
43 | if (it.endsWith("%"))
44 | Arg.Float(it.dropLast(1).toFloat() / 100)
45 | else Arg.Float(it.toFloat())
46 | }
47 | return Arg.Function("Color.rgba", params.take(3) + alpha)
48 | }
49 | return null
50 | }
51 |
52 | private fun hslOrNull(prop: String): Arg.Function? {
53 | val nums = parenContents(prop).split(' ', ',', '/').filter { it.isNotBlank() }
54 |
55 | val params = nums.take(3).mapIndexed { index, s ->
56 | if (index == 0) {
57 | Arg.UnitNum.ofOrNull(s, "deg") ?: Arg.UnitNum.of(s + "deg")
58 | } else {
59 | Arg.UnitNum.ofOrNull(s, "percent") ?: Arg.Float(s.toFloat())
60 | }
61 | }
62 | if (nums.size == 3) {
63 | return Arg.Function("Color.hsl", params)
64 | }
65 | if (nums.size == 4) {
66 | val alpha = nums.last().let {
67 | Arg.UnitNum.ofOrNull(it, "percent") ?: Arg.Float(it.toFloat())
68 | }
69 | return Arg.Function("Color.hsla", params.take(3) + alpha)
70 | }
71 | return null
72 | }
--------------------------------------------------------------------------------
/parsing/src/commonMain/kotlin/io/github/opletter/css2kobweb/functions/ConicGradient.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb.functions
2 |
3 | import io.github.opletter.css2kobweb.Arg
4 | import io.github.opletter.css2kobweb.splitNotInParens
5 |
6 | internal fun Arg.Function.Companion.conicGradient(value: String): Arg.Function {
7 | val parts = value.splitNotInParens(',')
8 | val argsAsColors = parts.mapNotNull { Arg.asColorOrNull(it) }
9 |
10 | val angle = Arg.UnitNum.ofOrNull(parts[0].substringBefore(" at ").substringAfter("from "), zeroUnit = "deg")
11 | val position = parts[0].substringAfter("at ", "")
12 | .takeIf { it.isNotEmpty() }
13 | ?.let { Arg.Function.position(it) }
14 |
15 | if (argsAsColors.size == 2) {
16 | return conicGradientOf(listOfNotNull(argsAsColors[0], argsAsColors[1], angle, position))
17 | }
18 |
19 | val mainArgs = listOfNotNull(angle, position)
20 | val lambdaFunctions = gradientColorStopList(parts.drop(if (mainArgs.isEmpty()) 0 else 1))
21 |
22 | return conicGradientOf(args = mainArgs, lambdaFunctions = lambdaFunctions)
23 | }
24 |
25 | private fun conicGradientOf(args: List, lambdaFunctions: List = emptyList()): Arg.Function =
26 | Arg.Function("conicGradient", args, lambdaFunctions)
--------------------------------------------------------------------------------
/parsing/src/commonMain/kotlin/io/github/opletter/css2kobweb/functions/Gradient.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb.functions
2 |
3 | import io.github.opletter.css2kobweb.Arg
4 | import io.github.opletter.css2kobweb.splitNotInParens
5 |
6 | internal fun gradientColorStopList(values: List): List {
7 | return values.map { colorStopList ->
8 | val subParts = colorStopList.splitNotInParens(' ')
9 | val unitParts = subParts.mapNotNull { Arg.UnitNum.ofOrNull(it, zeroUnit = "percent") }
10 |
11 | if (subParts.size == 1 && unitParts.size == 1) Arg.Function("setMidpoint", unitParts)
12 | else Arg.Function("add", listOf(Arg.asColorOrNull(subParts[0])!!) + unitParts)
13 | }
14 | }
--------------------------------------------------------------------------------
/parsing/src/commonMain/kotlin/io/github/opletter/css2kobweb/functions/LinearGradient.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb.functions
2 |
3 | import io.github.opletter.css2kobweb.Arg
4 | import io.github.opletter.css2kobweb.splitNotInParens
5 |
6 | internal fun Arg.Function.Companion.linearGradient(value: String): Arg.Function {
7 | val parts = value.splitNotInParens(',')
8 | val argsAsColors = parts.mapNotNull { Arg.asColorOrNull(it) }
9 | val firstAsUnitNum = Arg.UnitNum.ofOrNull(parts[0], zeroUnit = "deg")
10 | // check for color value, keeping in mind that there may be a percentage value in the arg
11 | val firstHasColor = Arg.asColorOrNull(parts[0].splitNotInParens(' ').first()) != null
12 |
13 | val (x, y) = parts[0].split(' ').partition { it == "left" || it == "right" }
14 | val direction = Arg.Property(
15 | "LinearGradient.Direction",
16 | (y + x).joinToString("") { it.replaceFirstChar(Char::uppercase) },
17 | )
18 |
19 | if (parts.size == 2 && argsAsColors.size == 2) {
20 | return linearGradientOf(argsAsColors)
21 | }
22 | if (parts.size == 3 && argsAsColors.size == 2) {
23 | if (firstAsUnitNum != null) {
24 | return linearGradientOf(argsAsColors + firstAsUnitNum)
25 | }
26 | if (!firstHasColor) {
27 | return linearGradientOf(argsAsColors + direction)
28 | }
29 | }
30 |
31 | val mainArg = if (!firstHasColor) firstAsUnitNum ?: direction else null
32 | val lambdaFunctions = gradientColorStopList(parts.drop(if (mainArg == null) 0 else 1))
33 |
34 | return linearGradientOf(args = listOfNotNull(mainArg), lambdaFunctions = lambdaFunctions)
35 | }
36 |
37 | private fun linearGradientOf(args: List, lambdaFunctions: List = emptyList()): Arg.Function =
38 | Arg.Function("linearGradient", args, lambdaFunctions)
--------------------------------------------------------------------------------
/parsing/src/commonMain/kotlin/io/github/opletter/css2kobweb/functions/Position.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb.functions
2 |
3 | import io.github.opletter.css2kobweb.Arg
4 | import io.github.opletter.css2kobweb.kebabToPascalCase
5 | import io.github.opletter.css2kobweb.splitNotInParens
6 |
7 | internal fun Arg.Function.Companion.positionOrNull(value: String): Arg? {
8 | val position = value.splitNotInParens(' ')
9 | val xEdges = setOf("left", "right")
10 | val yEdges = setOf("top", "bottom")
11 |
12 | return when (position.size) {
13 | 1 -> {
14 | val arg = position.single()
15 | Arg.UnitNum.ofOrNull(arg)?.let { Arg.Function("CSSPosition", it) }
16 | ?: Arg.Property.fromKebabValue("CSSPosition", arg)
17 | }
18 |
19 | 2 -> {
20 | val units = position.mapNotNull { Arg.UnitNum.ofOrNull(it) }
21 | when (units.size) {
22 | 0 -> {
23 | val notCenter = position.filter { it != "center" }
24 | when (notCenter.size) {
25 | 0 -> Arg.Property("CSSPosition", "Center")
26 | 1 -> Arg.Property.fromKebabValue("CSSPosition", notCenter.single())
27 | 2 -> {
28 | val (x, y) = notCenter.partition { it in xEdges }
29 | // nulls will be filtered out in validation step
30 | Arg.Property.fromKebabValue("CSSPosition", "${y.singleOrNull()}-${x.singleOrNull()}")
31 | }
32 |
33 | else -> error("Unexpected notCenter size: ${notCenter.size}")
34 | }
35 | }
36 |
37 | 1 -> {
38 | val centerIndex = position.indexOf("center")
39 | val xEdge = xEdges.singleOrNull { it in position }
40 | if (centerIndex != -1) {
41 | val x = if (centerIndex == 0) edge("center-x") else units.single()
42 | val y = if (centerIndex == 1) edge("center-y") else units.single()
43 | Arg.Function("CSSPosition", listOf(x, y))
44 | } else if (xEdge != null) {
45 | val yFun = Arg.Function("Edge.Top", units)
46 | Arg.Function("CSSPosition", listOf(edge(xEdge), yFun))
47 | } else {
48 | val yEdge = yEdges.singleOrNull { it in position } ?: return null
49 | val xFun = Arg.Function("Edge.Left", units)
50 | Arg.Function("CSSPosition", listOf(xFun, edge(yEdge)))
51 | }
52 | }
53 |
54 | 2 -> Arg.Function("CSSPosition", units)
55 |
56 | else -> error("Unexpected units size: ${units.size}")
57 | }
58 | }
59 |
60 | 3 -> { // could also delegate to 4-value logic but this lets us generate nicer code
61 | val xIndex = position.indexOfFirst { it in xEdges }
62 | .let { if (it == -1) position.indexOf("center") else it }
63 | val yIndex = position.indexOfFirst { it in yEdges }
64 | .let { if (it == -1) position.indexOf("center") else it }
65 |
66 | val unitIndex = ((0..2) - setOf(xIndex, yIndex)).singleOrNull() ?: return null
67 | val unit = Arg.UnitNum.ofOrNull(position[unitIndex]) ?: return null
68 |
69 | val xArg = if (xIndex + 1 == unitIndex) edge(position[xIndex], unit)
70 | else edge(position[xIndex].let { if (it == "center") "center-x" else it })
71 | val yArg = if (yIndex + 1 == unitIndex) edge(position[yIndex], unit)
72 | else edge(position[yIndex].let { if (it == "center") "center-y" else it })
73 |
74 | Arg.Function("CSSPosition", listOf(xArg, yArg))
75 | }
76 |
77 | 4 -> {
78 | val xIndex = if (position[0] in xEdges) 0 else 2
79 | val xUnit = Arg.UnitNum.ofOrNull(position[xIndex + 1]) ?: return null
80 | val yUnit = Arg.UnitNum.ofOrNull(position[3 - xIndex]) ?: return null
81 |
82 | Arg.Function("CSSPosition", listOf(edge(position[xIndex], xUnit), edge(position[2 - xIndex], yUnit)))
83 | }
84 |
85 | else -> null
86 | }?.takeIf {
87 | val validPositions = setOf(
88 | "Top", "TopRight", "Right", "BottomRight", "Bottom", "BottomLeft",
89 | "Left", "TopLeft", "Center"
90 | )
91 | !it.toString().startsWith("CSSPosition.")
92 | || it.toString().substringAfter(".") in validPositions
93 | }
94 | }
95 |
96 | internal fun Arg.Function.Companion.position(value: String): Arg =
97 | positionOrNull(value) ?: error("Invalid position: $value")
98 |
99 | private fun edge(name: String) = Arg.Property.fromKebabValue("Edge", name)
100 | private fun edge(name: String, unit: Arg.UnitNum) = Arg.Function("Edge.${kebabToPascalCase(name)}", unit)
--------------------------------------------------------------------------------
/parsing/src/commonMain/kotlin/io/github/opletter/css2kobweb/functions/RadialGradient.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb.functions
2 |
3 | import io.github.opletter.css2kobweb.Arg
4 | import io.github.opletter.css2kobweb.splitNotInParens
5 |
6 | internal fun Arg.Function.Companion.radialGradient(value: String): Arg.Function {
7 | val parts = value.splitNotInParens(',')
8 |
9 | val shape = parts[0].substringBefore(" at ")
10 | // check for color value, keeping in mind that there may be a percentage value in the arg
11 | .takeIf { Arg.asColorOrNull(it.splitNotInParens(' ')[0]) == null }
12 | ?.let { shapeStr ->
13 | val shapeParts = shapeStr.splitNotInParens(' ')
14 |
15 | val shape = if (shapeParts.any { it == "circle" }) "Circle" else "Ellipse"
16 | val shapeSize = (shapeParts - setOf("circle", "ellipse")).map {
17 | Arg.UnitNum.ofOrNull(it) ?: Arg.Property.fromKebabValue("RadialGradient.Extent", it)
18 | }
19 |
20 | if (shapeSize.isEmpty()) Arg.Property("RadialGradient.Shape", shape)
21 | else Arg.Function("RadialGradient.Shape.$shape", shapeSize)
22 | }
23 |
24 | val position = parts[0].substringAfter("at ", "").takeIf { it.isNotEmpty() }
25 | ?.let { Arg.Function.position(it) }
26 |
27 | val argsAsColors = parts.mapNotNull { Arg.asColorOrNull(it) }
28 | if (argsAsColors.size == 2) {
29 | return radialGradientOf(listOfNotNull(argsAsColors[0], argsAsColors[1], shape, position))
30 | }
31 |
32 | val mainArgs = listOfNotNull(shape, position)
33 | val lambdaFunctions = gradientColorStopList(parts.drop(if (mainArgs.isEmpty()) 0 else 1))
34 |
35 | return radialGradientOf(args = mainArgs, lambdaFunctions = lambdaFunctions)
36 | }
37 |
38 | private fun radialGradientOf(args: List, lambdaFunctions: List = emptyList()): Arg.Function =
39 | Arg.Function("radialGradient", args, lambdaFunctions)
--------------------------------------------------------------------------------
/parsing/src/commonMain/kotlin/io/github/opletter/css2kobweb/functions/Transition.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb.functions
2 |
3 | import io.github.opletter.css2kobweb.Arg
4 |
5 | internal fun Arg.Function.Companion.transition(
6 | property: Arg,
7 | duration: Arg? = null,
8 | remainingArgs: List = emptyList(),
9 | ): Arg.Function {
10 | val firstParams = listOfNotNull(property, duration)
11 |
12 | return when (remainingArgs.size) {
13 | 0, 2 -> transitionOf(firstParams + remainingArgs)
14 | 1 -> {
15 | val thirdArg = remainingArgs.single()
16 | .let { if (it is Arg.UnitNum) Arg.NamedArg("delay", it) else it }
17 | transitionOf(firstParams + thirdArg)
18 | }
19 |
20 | else -> error("Invalid transition")
21 | }
22 | }
23 |
24 | private fun transitionOf(args: List): Arg.Function {
25 | val function = if (args.first() is Arg.Function) "group" else "of"
26 | return Arg.Function("Transition.$function", args)
27 | }
--------------------------------------------------------------------------------
/parsing/src/jvmMain/kotlin/io/github/opletter/css2kobweb/DataCreation.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb
2 |
3 | import java.io.File
4 | import java.nio.file.Paths
5 | import kotlin.io.path.absolutePathString
6 |
7 | fun createUnitMappings() {
8 | println(Paths.get("").absolutePathString())
9 | val mappings = File("parsing/src/jvmMain/resources/units.txt").readText()
10 | .split("\n")
11 | .filter { it.isNotBlank() }
12 | .associate {
13 | val key = it.substringAfter("inline val ").substringBefore(' ')
14 | val rawStr = it.substringAfter('"').substringBefore('"')
15 |
16 | rawStr to key
17 | }
18 |
19 | mappings.forEach { (k, v) ->
20 | println("\"$k\" to \"$v\",")
21 | }
22 | }
23 |
24 | fun getColors() {
25 | File("parsing/src/jvmMain/resources/colors.txt").readLines()
26 | .joinToString("\", \"", "\"", "\"") {
27 | it.substringAfter("val ").substringBefore(" get()")
28 | }.also { println(it) }
29 | }
--------------------------------------------------------------------------------
/parsing/src/jvmMain/kotlin/io/github/opletter/css2kobweb/Main.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb
2 |
3 |
4 | fun main() {
5 | println(css2kobwebAsCode(rawCss3).joinToString(""))
6 | // println(css2kobweb(rawCSS2))
7 | }
8 |
9 | val rawCSS = """
10 | a {
11 | color: red;
12 | text-decoration: none;
13 | }
14 |
15 | .button, .test {
16 | background-color: #007bff;
17 | color: #ffffff;
18 | padding: 10px 20px;
19 | border-radius: 0;
20 | text-transform: uppercase;
21 | font-weight: bold;
22 | text-align: center;
23 | display: inline-block;
24 | margin: 10px;
25 | cursor: pointer;
26 | transition: margin-right 2s, color 1s;
27 | }
28 |
29 | .button:focus, .button:hover, .test:focus {
30 | background-color: #0056b3;
31 | }
32 | .button:focus-visible, .button > {
33 | background-color: rgb(0% 20% 50%);
34 | }
35 | .also-like-items > button > img {
36 | background-color: rgb(100% 0% 0%);
37 | }
38 |
39 | .screen:after,
40 | .screen:before {
41 | content: "";
42 | height: 5px;
43 | position: absolute;
44 | z-index: 4;
45 | left: 50%;
46 | translate: -50% 0%;
47 | background-color: white;
48 | }
49 |
50 | .screen:before, .screen:x {
51 | width: 15%;
52 | top: 0rem;
53 | border-bottom-left-radius: 1rem;
54 | border-bottom-right-radius: 1rem;
55 | }
56 |
57 | .screen:before, .screen:hover {
58 | width: 19%;
59 | }
60 |
61 | .screen:before {
62 | width: 19%;
63 | }
64 |
65 | .screen:z {
66 | height: 100%;
67 | }
68 | .screen:z {
69 | width: 200%;
70 | }
71 | .screen:z {
72 | border-radius: 0;
73 | }
74 | """.trimIndent()
75 |
76 | val rawCSS2 = """
77 | background-color: #007bff;
78 | color: #ffffff;
79 | padding: 10px 20px;
80 | border-radius: 0;
81 | text-transform: uppercase;
82 | font-weight: bold;
83 | text-align: center;
84 | display: inline-block;
85 | margin: 10px;
86 | cursor: pointer;
87 | transition: margin-right 2s, color 1s;
88 | """.trimIndent()
89 |
90 | val rawCss3 = """
91 | body {
92 | background-color: rgb(0,0,0);
93 | margin: 0px;
94 | }
95 |
96 | body::-webkit-scrollbar {
97 | width: 4px;
98 | }
99 |
100 | body::-webkit-scrollbar-track {
101 | background-color: rgb(1,1,1);
102 | }
103 |
104 | body::-webkit-scrollbar-thumb {
105 | background: rgba(255, 255, 255, 0.15);
106 | }
107 |
108 | * {
109 | box-sizing: border-box;
110 | margin: 0;
111 | padding: 0;
112 | }
113 |
114 | button {
115 | all: unset;
116 | cursor: pointer;
117 | }
118 |
119 | h1, h2, h3, h4, input, select, button, span, a, p {
120 | color: rgb(2,2,2);
121 | font-family: "Noto Sans", sans-serif;
122 | font-size: 1rem;
123 | }
124 |
125 | button, a, input {
126 | outline: none;
127 | }
128 |
129 | .highlight {
130 | color: rgb(3,3,3);
131 | }
132 |
133 | .gradient {
134 | background-image: rgb(4,4,4);
135 | -webkit-background-clip: text;
136 | -webkit-text-fill-color: transparent;
137 | }
138 |
139 | .fancy-scrollbar::-webkit-scrollbar {
140 | height: 4px;
141 | width: 4px;
142 | }
143 |
144 | .fancy-scrollbar::-webkit-scrollbar-track {
145 | background-color: transparent;
146 | }
147 |
148 | .fancy-scrollbar::-webkit-scrollbar-thumb {
149 | background: rgba(255, 255, 255, 0.15);
150 | }
151 |
152 | .no-scrollbar::-webkit-scrollbar {
153 | height: 0px;
154 | width: 0px;
155 | }
156 |
157 | #phone {
158 | box-shadow: rgba(0, 0, 0, 0.2) 0px 8px 24px;
159 | height: 851px;
160 | width: 393px;
161 | margin: 100px auto;
162 | position: relative;
163 | overflow: hidden;
164 | }
165 |
166 | #main-wrapper {
167 | height: 100%;
168 | overflow: auto;
169 | }
170 |
171 | #main {
172 | height: 100%;
173 | }
174 |
175 | #nav {
176 | width: 100%;
177 | display: flex;
178 | justify-content: space-around;
179 | position: absolute;
180 | left: 0px;
181 | bottom: 0px;
182 | z-index: 3;
183 | padding: 0.5rem 1rem;
184 | border-top: 1px solid rgb(255 255 255 / 10%);
185 | }
186 |
187 | #nav > button {
188 | padding: 0.5rem 1rem;
189 | border-radius: 0.25rem;
190 | position: relative;
191 | }
192 |
193 | #nav > button.active:after {
194 | content: "";
195 | height: 0.25rem;
196 | width: 1.5rem;
197 | position: absolute;
198 | top: -0.5rem;
199 | left: 50%;
200 | translate: -50%;
201 | border-bottom-left-radius: 0.25rem;
202 | border-bottom-right-radius: 0.25rem;
203 | }
204 |
205 | #nav > button:hover,
206 | #nav > button:focus-visible {
207 | background-color: rgb(255 255 255 / 10%);
208 | }
209 |
210 | #nav > button > i {
211 | width: 1.5rem;
212 | font-size: 1.1rem;
213 | text-align: center;
214 | }
215 |
216 | #header {
217 | display: flex;
218 | flex-direction: column;
219 | width: 100%;
220 | overflow: hidden;
221 | position: relative;
222 | }
223 |
224 | #header-background-image {
225 | width: 100%;
226 | display: flex;
227 | z-index: 1;
228 | left: 0px;
229 | top: 0px;
230 | position: relative;
231 | }
232 |
233 | #header-background-image > img {
234 | height: 100%;
235 | width: 100%;
236 | object-fit: cover;
237 | object-position: center;
238 | }
239 |
240 | #header-items {
241 | display: flex;
242 | gap: 1rem;
243 | position: relative;
244 | z-index: 3;
245 | padding: 0.5rem 1rem;
246 | overflow: auto;
247 | background: linear-gradient(to bottom, rgb(34, 123, 66) 0%, rgb(10 10 10) 40%, transparent 40%);
248 | }
249 |
250 | .header-item-image {
251 | position: relative;
252 | }
253 |
254 | .header-item-image:after {
255 | content: "";
256 | height: calc(100% - 0.5rem);
257 | width: calc(100% - 0.5rem);
258 | position: absolute;
259 | left: 0px;
260 | top: 0px;
261 | z-index: -1;
262 | background-color: white;
263 | margin: -0.25rem;
264 | border-radius: 0.5rem;
265 | box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;
266 | }
267 |
268 | .header-item-image > img {
269 | width: 200px;
270 | aspect-ratio: 16 / 9;
271 | object-fit: cover;
272 | object-position: center;
273 | border-radius: 0.4rem;
274 | }
275 |
276 | .header-item-content > .label {
277 | display: flex;
278 | }
279 |
280 | .header-item-content > .label > p {
281 | color: white;
282 | font-size: 0.75rem;
283 | font-weight: 500;
284 | margin-left: 0.25rem;
285 | }
286 |
287 | #mainBody {
288 | display: flex;
289 | flex-direction: column;
290 | gap: 1rem;
291 | }
292 |
293 | #body-content {
294 | display: flex;
295 | flex-direction: column;
296 | gap: 1rem;
297 | position: relative;
298 | z-index: 2;
299 | margin-top: 1rem;
300 | padding-bottom: 4rem;
301 | }
302 |
303 | #background-gradient {
304 | height: 300px;
305 | width: 100%;
306 | position: absolute;
307 | z-index: 1;
308 | background: linear-gradient(
309 | -15deg,
310 | transparent 30%,
311 | );
312 | opacity: 0.15;
313 | filter: blur(3rem);
314 | }
315 |
316 | #search-wrapper {
317 | display: flex;
318 | flex-direction: column;
319 | gap: 0.25rem;
320 | margin: 0rem 1rem;
321 | }
322 |
323 | #search {
324 | height: 3.5rem;
325 | position: relative;
326 | border-radius: 0.4rem;
327 | }
328 |
329 | #search-input {
330 | display: flex;
331 | align-items: center;
332 | gap: 0.5rem;
333 | position: absolute;
334 | inset: 2px;
335 | padding: 0.5rem;
336 | border-radius: 0.3rem;
337 | backdrop-filter: blur(0.75rem);
338 | }
339 |
340 | #search-input > i {
341 | width: 1.5rem;
342 | padding: 0rem 0.25rem;
343 | color: rgb(255 255 255 / 85%);
344 | text-align: center;
345 | }
346 |
347 | #search-input > input {
348 | width: 100%;
349 | flex-grow: 1;
350 | color: white;
351 | background-color: transparent;
352 | border: none;
353 | outline: none;
354 | font-weight: 500;
355 | }
356 |
357 | #search-input > button {
358 | height: 2rem;
359 | width: 2rem;
360 | display: grid;
361 | place-items: center;
362 | flex-shrink: 0;
363 | cursor: pointer;
364 | }
365 |
366 | #search-input > button > i {
367 | color: rgb(255 255 255 / 85%);
368 | }
369 |
370 | #search-input > button:is(:hover, :focus-visible) {
371 | background: rgb(255 255 255 / 10%);
372 | border-radius: 0.25rem;
373 | }
374 |
375 | #search-input > input::placeholder {
376 | color: rgb(255 255 255 / 25%);
377 | }
378 |
379 | #search-categories {
380 | display: flex;
381 | gap: 0.25rem;
382 | margin-bottom: 0.25rem;
383 | overflow: auto;
384 | }
385 |
386 | #search-categories > button {
387 | flex-shrink: 0;
388 | background-color: rgb(255 255 255 / 5%);
389 | padding: 0.5rem 0.75rem;
390 | border-radius: 0.25rem;
391 | color: white;
392 | font-size: 0.75rem;
393 | font-weight: 500;
394 | }
395 |
396 | #location > button {
397 | height: 2rem;
398 | display: flex;
399 | align-items: center;
400 | gap: 0.4rem;
401 | margin-left: 2.25rem;
402 | position: relative;
403 | }
404 |
405 | #location > button:after {
406 | content: "";
407 | height: 0.75rem;
408 | width: 0.5rem;
409 | position: absolute;
410 | left: 0px;
411 | top: 0px;
412 | margin-left: -1.25rem;
413 | margin-top: 0.25rem;
414 | border-left: 2px solid rgb(255 255 255 / 40%);
415 | border-bottom: 2px solid rgb(255 255 255 / 40%);
416 | border-bottom-left-radius: 0.3rem;
417 | }
418 |
419 | #location > button > :is(i, p) {
420 | display: flex;
421 | align-items: center;
422 | height: 100%;
423 | color: white;
424 | font-size: 0.75rem;
425 | }
426 |
427 | #location > button > i {
428 | height: 100%;
429 | }
430 |
431 | #location > button > p {
432 | color: rgb(5,5,5 / 75%);
433 | font-weight: 500;
434 | }
435 |
436 | #ad {
437 | display: flex;
438 | border: 1px solid rgb(255 255 255 / 10%);
439 | padding: 0.25rem;
440 | margin: 0rem 1rem;
441 | border-radius: 0.25rem;
442 | }
443 |
444 | #ad > img {
445 | width: 100%;
446 | border-radius: inherit;
447 | }
448 |
449 | #also-like {
450 | display: flex;
451 | flex-direction: column;
452 | gap: 0.5rem;
453 | margin: 0rem 1rem;
454 | }
455 |
456 | #also-like > h3 {
457 | font-size: 0.9rem;
458 | }
459 |
460 | #also-like-items {
461 | display: grid;
462 | gap: 0.5rem;
463 | grid-template-columns: repeat(3, auto);
464 | grid-template-rows: repeat(3, 1fr);
465 | }
466 |
467 | #also-like-items > button {
468 | display: flex;
469 | aspect-ratio: 1;
470 | }
471 |
472 | #also-like-items > button > img {
473 | height: 100%;
474 | width: 100%;
475 | object-fit: cover;
476 | border-radius: 0.25rem;
477 | }
478 |
479 | @media(max-width: 500px) {
480 | body {
481 | overflow: auto;
482 | }
483 |
484 | #phone {
485 | height: 100vh;
486 | display: flex;
487 | width: 100%;
488 | margin: 0px auto;
489 | }
490 |
491 | #main-wrapper {
492 | width: 100%;
493 | flex-grow: 1;
494 | }
495 | }
496 | """.trimIndent()
497 |
498 |
499 | val rawCss4 = """
500 | #search-input {
501 | display: flex;
502 | align-items: center;
503 | gap: 0.5rem;
504 | position: absolute;
505 | inset: 2px;
506 | padding: 0.5rem;
507 | border-radius: 0.3rem;
508 | backdrop-filter: blur(0.75rem);
509 | }
510 |
511 | #search-input > i {
512 | width: 1.5rem;
513 | padding: 0rem 0.25rem;
514 | color: rgb(255 255 255 / 85%);
515 | text-align: center;
516 | }
517 |
518 | #search-input > input {
519 | width: 100%;
520 | flex-grow: 1;
521 | color: white;
522 | background-color: transparent;
523 | border: none;
524 | outline: none;
525 | font-weight: 500;
526 | }
527 |
528 | #search-input > button, #search-input > custom {
529 | height: 2rem;
530 | width: 2rem;
531 | display: grid;
532 | place-items: center;
533 | flex-shrink: 0;
534 | cursor: pointer;
535 | }
536 |
537 | #search-input > button > i {
538 | color: rgb(255 255 255 / 85%);
539 | }
540 |
541 | #search-input > button:is(:hover, :focus-visible) {
542 | background: rgb(255 255 255 / 10%);
543 | border-radius: 0.25rem;
544 | }
545 |
546 | #search-input > input::placeholder {
547 | color: rgb(255 255 255 / 25%);
548 | }
549 | """.trimIndent()
--------------------------------------------------------------------------------
/parsing/src/jvmMain/resources/colors.txt:
--------------------------------------------------------------------------------
1 | val AliceBlue get() = Color.rgb(0xF0, 0xF8, 0xFF)
2 | val AntiqueWhite get() = Color.rgb(0xFA, 0xEB, 0xD7)
3 | val Aqua get() = Color.rgb(0x00, 0xFF, 0xFF)
4 | val Aquamarine get() = Color.rgb(0x7F, 0xFF, 0xD4)
5 | val Azure get() = Color.rgb(0xF0, 0xFF, 0xFF)
6 | val Beige get() = Color.rgb(0xF5, 0xF5, 0xDC)
7 | val Bisque get() = Color.rgb(0xFF, 0xE4, 0xC4)
8 | val Black get() = Color.rgb(0x00, 0x00, 0x00)
9 | val BlanchedAlmond get() = Color.rgb(0xFF, 0xEB, 0xCD)
10 | val Blue get() = Color.rgb(0x00, 0x00, 0xFF)
11 | val BlueViolet get() = Color.rgb(0x8A, 0x2B, 0xE2)
12 | val Brown get() = Color.rgb(0xA5, 0x2A, 0x2A)
13 | val BurlyWood get() = Color.rgb(0xDE, 0xB8, 0x87)
14 | val CadetBlue get() = Color.rgb(0x5F, 0x9E, 0xA0)
15 | val Chartreuse get() = Color.rgb(0x7F, 0xFF, 0x00)
16 | val Chocolate get() = Color.rgb(0xD2, 0x69, 0x1E)
17 | val Coral get() = Color.rgb(0xFF, 0x7F, 0x50)
18 | val CornflowerBlue get() = Color.rgb(0x64, 0x95, 0xED)
19 | val Cornsilk get() = Color.rgb(0xFF, 0xF8, 0xDC)
20 | val Crimson get() = Color.rgb(0xDC, 0x14, 0x3C)
21 | val Cyan get() = Color.rgb(0x00, 0xFF, 0xFF)
22 | val DarkBlue get() = Color.rgb(0x00, 0x00, 0x8B)
23 | val DarkCyan get() = Color.rgb(0x00, 0x8B, 0x8B)
24 | val DarkGoldenRod get() = Color.rgb(0xB8, 0x86, 0x0B)
25 | val DarkGray get() = Color.rgb(0xA9, 0xA9, 0xA9)
26 | val DarkGrey get() = Color.rgb(0xA9, 0xA9, 0xA9)
27 | val DarkGreen get() = Color.rgb(0x00, 0x64, 0x00)
28 | val DarkKhaki get() = Color.rgb(0xBD, 0xB7, 0x6B)
29 | val DarkMagenta get() = Color.rgb(0x8B, 0x00, 0x8B)
30 | val DarkOliveGreen get() = Color.rgb(0x55, 0x6B, 0x2F)
31 | val DarkOrange get() = Color.rgb(0xFF, 0x8C, 0x00)
32 | val DarkOrchid get() = Color.rgb(0x99, 0x32, 0xCC)
33 | val DarkRed get() = Color.rgb(0x8B, 0x00, 0x00)
34 | val DarkSalmon get() = Color.rgb(0xE9, 0x96, 0x7A)
35 | val DarkSeaGreen get() = Color.rgb(0x8F, 0xBC, 0x8F)
36 | val DarkSlateBlue get() = Color.rgb(0x48, 0x3D, 0x8B)
37 | val DarkSlateGray get() = Color.rgb(0x2F, 0x4F, 0x4F)
38 | val DarkSlateGrey get() = Color.rgb(0x2F, 0x4F, 0x4F)
39 | val DarkTurquoise get() = Color.rgb(0x00, 0xCE, 0xD1)
40 | val DarkViolet get() = Color.rgb(0x94, 0x00, 0xD3)
41 | val DeepPink get() = Color.rgb(0xFF, 0x14, 0x93)
42 | val DeepSkyBlue get() = Color.rgb(0x00, 0xBF, 0xFF)
43 | val DimGray get() = Color.rgb(0x69, 0x69, 0x69)
44 | val DimGrey get() = Color.rgb(0x69, 0x69, 0x69)
45 | val DodgerBlue get() = Color.rgb(0x1E, 0x90, 0xFF)
46 | val FireBrick get() = Color.rgb(0xB2, 0x22, 0x22)
47 | val FloralWhite get() = Color.rgb(0xFF, 0xFA, 0xF0)
48 | val ForestGreen get() = Color.rgb(0x22, 0x8B, 0x22)
49 | val Fuchsia get() = Color.rgb(0xFF, 0x00, 0xFF)
50 | val Gainsboro get() = Color.rgb(0xDC, 0xDC, 0xDC)
51 | val GhostWhite get() = Color.rgb(0xF8, 0xF8, 0xFF)
52 | val Gold get() = Color.rgb(0xFF, 0xD7, 0x00)
53 | val GoldenRod get() = Color.rgb(0xDA, 0xA5, 0x20)
54 | val Gray get() = Color.rgb(0x80, 0x80, 0x80)
55 | val Grey get() = Color.rgb(0x80, 0x80, 0x80)
56 | val Green get() = Color.rgb(0x00, 0x80, 0x00)
57 | val GreenYellow get() = Color.rgb(0xAD, 0xFF, 0x2F)
58 | val HoneyDew get() = Color.rgb(0xF0, 0xFF, 0xF0)
59 | val HotPink get() = Color.rgb(0xFF, 0x69, 0xB4)
60 | val IndianRed get() = Color.rgb(0xCD, 0x5C, 0x5C)
61 | val Indigo get() = Color.rgb(0x4B, 0x00, 0x82)
62 | val Ivory get() = Color.rgb(0xFF, 0xFF, 0xF0)
63 | val Khaki get() = Color.rgb(0xF0, 0xE6, 0x8C)
64 | val Lavender get() = Color.rgb(0xE6, 0xE6, 0xFA)
65 | val LavenderBlush get() = Color.rgb(0xFF, 0xF0, 0xF5)
66 | val LawnGreen get() = Color.rgb(0x7C, 0xFC, 0x00)
67 | val LemonChiffon get() = Color.rgb(0xFF, 0xFA, 0xCD)
68 | val LightBlue get() = Color.rgb(0xAD, 0xD8, 0xE6)
69 | val LightCoral get() = Color.rgb(0xF0, 0x80, 0x80)
70 | val LightCyan get() = Color.rgb(0xE0, 0xFF, 0xFF)
71 | val LightGoldenRodYellow get() = Color.rgb(0xFA, 0xFA, 0xD2)
72 | val LightGray get() = Color.rgb(0xD3, 0xD3, 0xD3)
73 | val LightGrey get() = Color.rgb(0xD3, 0xD3, 0xD3)
74 | val LightGreen get() = Color.rgb(0x90, 0xEE, 0x90)
75 | val LightPink get() = Color.rgb(0xFF, 0xB6, 0xC1)
76 | val LightSalmon get() = Color.rgb(0xFF, 0xA0, 0x7A)
77 | val LightSeaGreen get() = Color.rgb(0x20, 0xB2, 0xAA)
78 | val LightSkyBlue get() = Color.rgb(0x87, 0xCE, 0xFA)
79 | val LightSlateGray get() = Color.rgb(0x77, 0x88, 0x99)
80 | val LightSlateGrey get() = Color.rgb(0x77, 0x88, 0x99)
81 | val LightSteelBlue get() = Color.rgb(0xB0, 0xC4, 0xDE)
82 | val LightYellow get() = Color.rgb(0xFF, 0xFF, 0xE0)
83 | val Lime get() = Color.rgb(0x00, 0xFF, 0x00)
84 | val LimeGreen get() = Color.rgb(0x32, 0xCD, 0x32)
85 | val Linen get() = Color.rgb(0xFA, 0xF0, 0xE6)
86 | val Magenta get() = Color.rgb(0xFF, 0x00, 0xFF)
87 | val Maroon get() = Color.rgb(0x80, 0x00, 0x00)
88 | val MediumAquaMarine get() = Color.rgb(0x66, 0xCD, 0xAA)
89 | val MediumBlue get() = Color.rgb(0x00, 0x00, 0xCD)
90 | val MediumOrchid get() = Color.rgb(0xBA, 0x55, 0xD3)
91 | val MediumPurple get() = Color.rgb(0x93, 0x70, 0xDB)
92 | val MediumSeaGreen get() = Color.rgb(0x3C, 0xB3, 0x71)
93 | val MediumSlateBlue get() = Color.rgb(0x7B, 0x68, 0xEE)
94 | val MediumSpringGreen get() = Color.rgb(0x00, 0xFA, 0x9A)
95 | val MediumTurquoise get() = Color.rgb(0x48, 0xD1, 0xCC)
96 | val MediumVioletRed get() = Color.rgb(0xC7, 0x15, 0x85)
97 | val MidnightBlue get() = Color.rgb(0x19, 0x19, 0x70)
98 | val MintCream get() = Color.rgb(0xF5, 0xFF, 0xFA)
99 | val MistyRose get() = Color.rgb(0xFF, 0xE4, 0xE1)
100 | val Moccasin get() = Color.rgb(0xFF, 0xE4, 0xB5)
101 | val NavajoWhite get() = Color.rgb(0xFF, 0xDE, 0xAD)
102 | val Navy get() = Color.rgb(0x00, 0x00, 0x80)
103 | val OldLace get() = Color.rgb(0xFD, 0xF5, 0xE6)
104 | val Olive get() = Color.rgb(0x80, 0x80, 0x00)
105 | val OliveDrab get() = Color.rgb(0x6B, 0x8E, 0x23)
106 | val Orange get() = Color.rgb(0xFF, 0xA5, 0x00)
107 | val OrangeRed get() = Color.rgb(0xFF, 0x45, 0x00)
108 | val Orchid get() = Color.rgb(0xDA, 0x70, 0xD6)
109 | val PaleGoldenRod get() = Color.rgb(0xEE, 0xE8, 0xAA)
110 | val PaleGreen get() = Color.rgb(0x98, 0xFB, 0x98)
111 | val PaleTurquoise get() = Color.rgb(0xAF, 0xEE, 0xEE)
112 | val PaleVioletRed get() = Color.rgb(0xDB, 0x70, 0x93)
113 | val PapayaWhip get() = Color.rgb(0xFF, 0xEF, 0xD5)
114 | val PeachPuff get() = Color.rgb(0xFF, 0xDA, 0xB9)
115 | val Peru get() = Color.rgb(0xCD, 0x85, 0x3F)
116 | val Pink get() = Color.rgb(0xFF, 0xC0, 0xCB)
117 | val Plum get() = Color.rgb(0xDD, 0xA0, 0xDD)
118 | val PowderBlue get() = Color.rgb(0xB0, 0xE0, 0xE6)
119 | val Purple get() = Color.rgb(0x80, 0x00, 0x80)
120 | val RebeccaPurple get() = Color.rgb(0x66, 0x33, 0x99)
121 | val Red get() = Color.rgb(0xFF, 0x00, 0x00)
122 | val RosyBrown get() = Color.rgb(0xBC, 0x8F, 0x8F)
123 | val RoyalBlue get() = Color.rgb(0x41, 0x69, 0xE1)
124 | val SaddleBrown get() = Color.rgb(0x8B, 0x45, 0x13)
125 | val Salmon get() = Color.rgb(0xFA, 0x80, 0x72)
126 | val SandyBrown get() = Color.rgb(0xF4, 0xA4, 0x60)
127 | val SeaGreen get() = Color.rgb(0x2E, 0x8B, 0x57)
128 | val SeaShell get() = Color.rgb(0xFF, 0xF5, 0xEE)
129 | val Sienna get() = Color.rgb(0xA0, 0x52, 0x2D)
130 | val Silver get() = Color.rgb(0xC0, 0xC0, 0xC0)
131 | val SkyBlue get() = Color.rgb(0x87, 0xCE, 0xEB)
132 | val SlateBlue get() = Color.rgb(0x6A, 0x5A, 0xCD)
133 | val SlateGray get() = Color.rgb(0x70, 0x80, 0x90)
134 | val SlateGrey get() = Color.rgb(0x70, 0x80, 0x90)
135 | val Snow get() = Color.rgb(0xFF, 0xFA, 0xFA)
136 | val SpringGreen get() = Color.rgb(0x00, 0xFF, 0x7F)
137 | val SteelBlue get() = Color.rgb(0x46, 0x82, 0xB4)
138 | val Tan get() = Color.rgb(0xD2, 0xB4, 0x8C)
139 | val Teal get() = Color.rgb(0x00, 0x80, 0x80)
140 | val Thistle get() = Color.rgb(0xD8, 0xBF, 0xD8)
141 | val Tomato get() = Color.rgb(0xFF, 0x63, 0x47)
142 | val Turquoise get() = Color.rgb(0x40, 0xE0, 0xD0)
143 | val Violet get() = Color.rgb(0xEE, 0x82, 0xEE)
144 | val Wheat get() = Color.rgb(0xF5, 0xDE, 0xB3)
145 | val White get() = Color.rgb(0xFF, 0xFF, 0xFF)
146 | val WhiteSmoke get() = Color.rgb(0xF5, 0xF5, 0xF5)
147 | val Yellow get() = Color.rgb(0xFF, 0xFF, 0x00)
148 | val YellowGreen get() = Color.rgb(0x9A, 0xCD, 0x32)
--------------------------------------------------------------------------------
/parsing/src/jvmMain/resources/units.txt:
--------------------------------------------------------------------------------
1 | inline val percent get() = "%".unsafeCast()
2 |
3 | inline val em get() = "em".unsafeCast()
4 |
5 | inline val ex get() = "ex".unsafeCast()
6 |
7 | inline val ch get() = "ch".unsafeCast()
8 |
9 | inline val ic get() = "ic".unsafeCast()
10 |
11 | inline val cssRem get() = "rem".unsafeCast() // manually edited
12 |
13 | inline val lh get() = "lh".unsafeCast()
14 |
15 | inline val rlh get() = "rlh".unsafeCast()
16 |
17 | inline val vw get() = "vw".unsafeCast()
18 |
19 | inline val vh get() = "vh".unsafeCast()
20 |
21 | inline val vi get() = "vi".unsafeCast()
22 |
23 | inline val vb get() = "vb".unsafeCast()
24 |
25 | inline val vmin get() = "vmin".unsafeCast()
26 |
27 | inline val vmax get() = "vmax".unsafeCast()
28 |
29 | inline val cm get() = "cm".unsafeCast()
30 |
31 | inline val mm get() = "mm".unsafeCast()
32 |
33 | inline val Q get() = "Q".unsafeCast()
34 |
35 | inline val pt get() = "pt".unsafeCast()
36 |
37 | inline val pc get() = "pc".unsafeCast()
38 |
39 | inline val px get() = "px".unsafeCast()
40 |
41 | inline val deg get() = "deg".unsafeCast()
42 |
43 | inline val grad get() = "grad".unsafeCast()
44 |
45 | inline val rad get() = "rad".unsafeCast()
46 |
47 | inline val turn get() = "turn".unsafeCast()
48 |
49 | inline val s get() = "s".unsafeCast()
50 |
51 | inline val ms get() = "ms".unsafeCast()
52 |
53 | inline val Hz get() = "Hz".unsafeCast()
54 |
55 | inline val kHz get() = "kHz".unsafeCast()
56 |
57 | inline val dpi get() = "dpi".unsafeCast()
58 |
59 | inline val dpcm get() = "dpcm".unsafeCast()
60 |
61 | inline val dppx get() = "dppx".unsafeCast()
62 |
63 | inline val fr get() = "fr".unsafeCast()
64 |
65 | inline val number get() = "number".unsafeCast()
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | }
5 | }
6 |
7 | dependencyResolutionManagement {
8 | @Suppress("UnstableApiUsage")
9 | repositories {
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
15 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
16 |
17 | rootProject.name = "css2kobweb"
18 |
19 | include(":site")
20 | include(":parsing")
--------------------------------------------------------------------------------
/site/.gitignore:
--------------------------------------------------------------------------------
1 | # Kobweb ignores
2 | .kobweb/*
3 | !.kobweb/conf.yaml
4 |
--------------------------------------------------------------------------------
/site/.kobweb/conf.yaml:
--------------------------------------------------------------------------------
1 | site:
2 | title: "css2kobweb"
3 | basePath: "css2kobweb" # repo name for gh-pages
4 |
5 | server:
6 | files:
7 | dev:
8 | contentRoot: "build/processedResources/js/main/public"
9 | script: "build/kotlin-webpack/js/developmentExecutable/css2kobweb.js"
10 | api: "build/libs/css2kobweb.jar"
11 | prod:
12 | script: "build/kotlin-webpack/js/productionExecutable/css2kobweb.js"
13 | siteRoot: ".kobweb/site"
14 |
15 | port: 8080
--------------------------------------------------------------------------------
/site/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.varabyte.kobweb.gradle.application.util.configAsKobwebApplication
2 |
3 | plugins {
4 | alias(libs.plugins.kotlin.multiplatform)
5 | alias(libs.plugins.compose.compiler)
6 | alias(libs.plugins.kobweb.application)
7 | }
8 |
9 | group = "io.github.opletter.css2kobweb"
10 | version = "1.0-SNAPSHOT"
11 |
12 | kobweb {
13 | app {
14 | index {
15 | description = "Convert CSS to Kobweb modifiers & styles"
16 | }
17 | }
18 | }
19 |
20 | kotlin {
21 | configAsKobwebApplication("css2kobweb")
22 | js().compilerOptions {
23 | target = "es2015"
24 | freeCompilerArgs.add("-Xir-generate-inline-anonymous-functions")
25 | }
26 |
27 | sourceSets {
28 | jsMain.dependencies {
29 | implementation(libs.compose.runtime)
30 | implementation(libs.compose.html.core)
31 | implementation(libs.kobweb.core)
32 | implementation(libs.kobweb.silk)
33 | implementation(libs.silk.icons.fa)
34 | implementation(projects.parsing)
35 | }
36 | }
37 | }
38 |
39 | composeCompiler {
40 | includeTraceMarkers = false
41 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/io/github/opletter/css2kobweb/AppEntry.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.varabyte.kobweb.compose.ui.Modifier
5 | import com.varabyte.kobweb.compose.ui.graphics.Color
6 | import com.varabyte.kobweb.compose.ui.modifiers.fontFamily
7 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize
8 | import com.varabyte.kobweb.compose.ui.modifiers.marginBlock
9 | import com.varabyte.kobweb.compose.ui.modifiers.minHeight
10 | import com.varabyte.kobweb.core.App
11 | import com.varabyte.kobweb.silk.SilkApp
12 | import com.varabyte.kobweb.silk.components.layout.Surface
13 | import com.varabyte.kobweb.silk.init.InitSilk
14 | import com.varabyte.kobweb.silk.init.InitSilkContext
15 | import com.varabyte.kobweb.silk.init.registerStyleBase
16 | import com.varabyte.kobweb.silk.style.breakpoint.Breakpoint
17 | import com.varabyte.kobweb.silk.style.common.SmoothColorStyle
18 | import com.varabyte.kobweb.silk.style.toModifier
19 | import com.varabyte.kobweb.silk.theme.colors.palette.background
20 | import com.varabyte.kobweb.silk.theme.colors.palette.button
21 | import com.varabyte.kobweb.silk.theme.colors.palette.color
22 | import org.jetbrains.compose.web.css.cssRem
23 | import org.jetbrains.compose.web.css.vh
24 |
25 | @InitSilk
26 | fun updateTheme(ctx: InitSilkContext) {
27 | with(ctx.stylesheet) {
28 | registerStyleBase("body") {
29 | Modifier.fontFamily("system-ui", "Segoe UI", "Tahoma", "Helvetica", "sans-serif")
30 | }
31 | registerStyle("h1") {
32 | base {
33 | Modifier
34 | .fontSize(2.5.cssRem)
35 | .marginBlock(0.5.cssRem, 0.5.cssRem)
36 | }
37 | Breakpoint.MD {
38 | Modifier.fontSize(2.75.cssRem)
39 | }
40 | }
41 | registerStyleBase("h2") {
42 | Modifier.marginBlock(0.cssRem, 0.cssRem)
43 | }
44 | }
45 |
46 | // https://coolors.co/1e1f22-3f334d-8bdbe2-f97068-f7e733
47 | // todo: unique color modes
48 | ctx.theme.palettes.light.apply {
49 | background = Color.rgb(188, 190, 196)
50 | color = Color.rgb(30, 31, 34)
51 | button.set(
52 | default = Color.rgb(0xF97068),
53 | hover = Color.rgb(0xF97068).darkened(0.1f),
54 | focus = ctx.theme.palettes.light.button.focus,
55 | pressed = Color.rgb(0xF97068).darkened(0.25f),
56 | )
57 | }
58 | }
59 |
60 | @App
61 | @Composable
62 | fun AppEntry(content: @Composable () -> Unit) {
63 | SilkApp {
64 | Surface(SmoothColorStyle.toModifier().minHeight(100.vh)) {
65 | content()
66 | }
67 | }
68 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/io/github/opletter/css2kobweb/components/layouts/PageLayout.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb.components.layouts
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.varabyte.kobweb.compose.foundation.layout.Box
5 | import com.varabyte.kobweb.compose.foundation.layout.Column
6 | import com.varabyte.kobweb.compose.ui.Alignment
7 | import com.varabyte.kobweb.compose.ui.Modifier
8 | import com.varabyte.kobweb.compose.ui.modifiers.*
9 | import com.varabyte.kobweb.silk.style.toModifier
10 | import io.github.opletter.css2kobweb.components.sections.Footer
11 | import io.github.opletter.css2kobweb.components.styles.BackgroundGradientStyle
12 | import org.jetbrains.compose.web.css.fr
13 | import org.jetbrains.compose.web.css.percent
14 | import org.jetbrains.compose.web.dom.H1
15 | import org.jetbrains.compose.web.dom.Text
16 |
17 | @Composable
18 | fun PageLayout(title: String, content: @Composable () -> Unit) {
19 | // Create a box with two rows: the main content (fills as much space as it can) and the footer (which reserves
20 | // space at the bottom). "auto" means the use the height of the row. "1fr" means give the rest of the space to
21 | // that row. Since this box is set to *at least* 100%, the footer will always appear at least on the bottom but
22 | // can be pushed further down if the first row grows beyond the page.
23 | Box(
24 | BackgroundGradientStyle.toModifier()
25 | .fillMaxSize()
26 | .gridTemplateRows { size(1.fr); size(auto) },
27 | contentAlignment = Alignment.TopCenter
28 | ) {
29 | Column(
30 | modifier = Modifier
31 | .fillMaxHeight()
32 | .width(90.percent),
33 | horizontalAlignment = Alignment.CenterHorizontally,
34 | ) {
35 | H1 { Text(title) }
36 | content()
37 | }
38 | // Associate the footer with the row that will get pushed off the bottom of the page if it can't fit.
39 | Footer(Modifier.gridRowStart(2).gridRowEnd(3))
40 | }
41 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/io/github/opletter/css2kobweb/components/sections/Footer.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb.components.sections
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.varabyte.kobweb.compose.css.AlignSelf
5 | import com.varabyte.kobweb.compose.css.TextAlign
6 | import com.varabyte.kobweb.compose.foundation.layout.Column
7 | import com.varabyte.kobweb.compose.foundation.layout.Row
8 | import com.varabyte.kobweb.compose.ui.Alignment
9 | import com.varabyte.kobweb.compose.ui.Modifier
10 | import com.varabyte.kobweb.compose.ui.modifiers.alignSelf
11 | import com.varabyte.kobweb.compose.ui.modifiers.margin
12 | import com.varabyte.kobweb.compose.ui.modifiers.rowGap
13 | import com.varabyte.kobweb.compose.ui.modifiers.textAlign
14 | import com.varabyte.kobweb.silk.components.icons.fa.FaGithub
15 | import com.varabyte.kobweb.silk.components.navigation.Link
16 | import com.varabyte.kobweb.silk.components.text.SpanText
17 | import com.varabyte.kobweb.silk.style.CssStyle
18 | import com.varabyte.kobweb.silk.style.base
19 | import com.varabyte.kobweb.silk.style.toModifier
20 | import org.jetbrains.compose.web.css.cssRem
21 |
22 | val FooterStyle = CssStyle.base {
23 | Modifier
24 | .margin(topBottom = 1.cssRem)
25 | .alignSelf(AlignSelf.Center)
26 | .textAlign(TextAlign.Center)
27 | .rowGap(0.1.cssRem)
28 | }
29 |
30 | @Composable
31 | fun Footer(modifier: Modifier = Modifier) {
32 | Column(
33 | FooterStyle.toModifier().then(modifier),
34 | horizontalAlignment = Alignment.CenterHorizontally,
35 | ) {
36 | Row(verticalAlignment = Alignment.CenterVertically) {
37 | FaGithub()
38 | SpanText(" This site is ")
39 | Link(path = "https://github.com/opLetter/css2kobweb", text = "open source")
40 | }
41 | Row {
42 | SpanText("Made with ")
43 | Link(path = "https://github.com/varabyte/kobweb", text = "Kobweb")
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/io/github/opletter/css2kobweb/components/styles/Background.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb.components.styles
2 |
3 | import com.varabyte.kobweb.compose.css.Background
4 | import com.varabyte.kobweb.compose.css.CSSPosition
5 | import com.varabyte.kobweb.compose.css.functions.RadialGradient
6 | import com.varabyte.kobweb.compose.css.functions.linearGradient
7 | import com.varabyte.kobweb.compose.css.functions.radialGradient
8 | import com.varabyte.kobweb.compose.css.functions.toImage
9 | import com.varabyte.kobweb.compose.ui.Modifier
10 | import com.varabyte.kobweb.compose.ui.graphics.Color
11 | import com.varabyte.kobweb.compose.ui.modifiers.background
12 | import com.varabyte.kobweb.silk.style.CssStyle
13 | import com.varabyte.kobweb.silk.style.base
14 | import org.jetbrains.compose.web.css.deg
15 | import org.jetbrains.compose.web.css.percent
16 |
17 | // Image courtesy of gradientmagic.com
18 | // https://www.gradientmagic.com/collection/popular/gradient/1583693118025
19 | // translated with css2kobweb :)
20 | val BackgroundGradientStyle = CssStyle.base {
21 | Modifier
22 | .background(
23 | Background.of(image = linearGradient(Color.rgb(34, 222, 237), Color.rgb(135, 89, 215), 90.deg).toImage()),
24 | Background.of(
25 | image = radialGradient(RadialGradient.Shape.Circle, CSSPosition(75.percent, 99.percent)) {
26 | add(Color.rgba(243, 243, 243, 0.04f), 0.percent)
27 | add(Color.rgba(243, 243, 243, 0.04f), 50.percent)
28 | add(Color.rgba(37, 37, 37, 0.04f), 50.percent)
29 | add(Color.rgba(37, 37, 37, 0.04f), 100.percent)
30 | }.toImage()
31 | ),
32 | Background.of(
33 | image = radialGradient(RadialGradient.Shape.Circle, CSSPosition(15.percent, 16.percent)) {
34 | add(Color.rgba(99, 99, 99, 0.04f), 0.percent)
35 | add(Color.rgba(99, 99, 99, 0.04f), 50.percent)
36 | add(Color.rgba(45, 45, 45, 0.04f), 50.percent)
37 | add(Color.rgba(45, 45, 45, 0.04f), 100.percent)
38 | }.toImage()
39 | ),
40 | Background.of(
41 | image = radialGradient(RadialGradient.Shape.Circle, CSSPosition(86.percent, 7.percent)) {
42 | add(Color.rgba(40, 40, 40, 0.04f), 0.percent)
43 | add(Color.rgba(40, 40, 40, 0.04f), 50.percent)
44 | add(Color.rgba(200, 200, 200, 0.04f), 50.percent)
45 | add(Color.rgba(200, 200, 200, 0.04f), 100.percent)
46 | }.toImage()
47 | ),
48 | Background.of(
49 | image = radialGradient(RadialGradient.Shape.Circle, CSSPosition(66.percent, 97.percent)) {
50 | add(Color.rgba(36, 36, 36, 0.04f), 0.percent)
51 | add(Color.rgba(36, 36, 36, 0.04f), 50.percent)
52 | add(Color.rgba(46, 46, 46, 0.04f), 50.percent)
53 | add(Color.rgba(46, 46, 46, 0.04f), 100.percent)
54 | }.toImage()
55 | ),
56 | Background.of(
57 | image = radialGradient(RadialGradient.Shape.Circle, CSSPosition(40.percent, 91.percent)) {
58 | add(Color.rgba(251, 251, 251, 0.04f), 0.percent)
59 | add(Color.rgba(251, 251, 251, 0.04f), 50.percent)
60 | add(Color.rgba(229, 229, 229, 0.04f), 50.percent)
61 | add(Color.rgba(229, 229, 229, 0.04f), 100.percent)
62 | }.toImage()
63 | )
64 | )
65 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/io/github/opletter/css2kobweb/components/widgets/KotlinCode.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb.components.widgets
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.varabyte.kobweb.compose.dom.ref
5 | import com.varabyte.kobweb.compose.dom.registerRefScope
6 | import com.varabyte.kobweb.compose.ui.Modifier
7 | import com.varabyte.kobweb.compose.ui.graphics.Color
8 | import com.varabyte.kobweb.compose.ui.modifiers.margin
9 | import com.varabyte.kobweb.compose.ui.toAttrs
10 | import io.github.opletter.css2kobweb.CodeBlock
11 | import io.github.opletter.css2kobweb.CodeElement
12 | import org.jetbrains.compose.web.css.px
13 | import org.jetbrains.compose.web.dom.Pre
14 |
15 | @Composable
16 | fun KotlinCode(code: List, modifier: Modifier = Modifier) {
17 | Pre(Modifier.margin(0.px).then(modifier).toAttrs()) {
18 | registerRefScope(ref(code) {
19 | it.innerHTML = code.convertToHtml()
20 | })
21 | }
22 | }
23 |
24 | private object ColorScheme {
25 | val keyword = Color.rgb(0xcF8E6D)
26 | val property = Color.rgb(0xC77DBB)
27 | val extensionFun = Color.rgb(0x56A8F5)
28 | val string = Color.rgb(0x6AAB73)
29 | val number = Color.rgb(0x2AACB8)
30 | val namedArg = Color.rgb(0x56C1D6)
31 | }
32 |
33 | private fun colorForType(type: CodeElement): Color = when (type) {
34 | CodeElement.Keyword -> ColorScheme.keyword
35 | CodeElement.Property -> ColorScheme.property
36 | CodeElement.ExtensionFun -> ColorScheme.extensionFun
37 | CodeElement.String -> ColorScheme.string
38 | CodeElement.Number -> ColorScheme.number
39 | CodeElement.NamedArg -> ColorScheme.namedArg
40 | CodeElement.Plain -> error("Should not be plain")
41 | }
42 |
43 | private fun List.convertToHtml(): String = joinToString("") { block ->
44 | if (block.type == CodeElement.Plain) {
45 | block.text.htmlEscape()
46 | } else {
47 | """${block.text.htmlEscape()}"""
48 | }
49 | }
50 |
51 | private fun String.htmlEscape(): String = this.replace("&", "&").replace("<", "<")
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/io/github/opletter/css2kobweb/pages/Index.kt:
--------------------------------------------------------------------------------
1 | package io.github.opletter.css2kobweb.pages
2 |
3 | import androidx.compose.runtime.*
4 | import com.varabyte.kobweb.compose.css.Overflow
5 | import com.varabyte.kobweb.compose.css.OverflowWrap
6 | import com.varabyte.kobweb.compose.css.Resize
7 | import com.varabyte.kobweb.compose.foundation.layout.Column
8 | import com.varabyte.kobweb.compose.foundation.layout.Row
9 | import com.varabyte.kobweb.compose.ui.Alignment
10 | import com.varabyte.kobweb.compose.ui.Modifier
11 | import com.varabyte.kobweb.compose.ui.graphics.Color
12 | import com.varabyte.kobweb.compose.ui.graphics.Colors
13 | import com.varabyte.kobweb.compose.ui.modifiers.*
14 | import com.varabyte.kobweb.compose.ui.styleModifier
15 | import com.varabyte.kobweb.compose.ui.toAttrs
16 | import com.varabyte.kobweb.core.Page
17 | import com.varabyte.kobweb.silk.components.forms.Button
18 | import com.varabyte.kobweb.silk.components.forms.ButtonSize
19 | import com.varabyte.kobweb.silk.components.icons.fa.FaTrashCan
20 | import com.varabyte.kobweb.silk.components.layout.SimpleGrid
21 | import com.varabyte.kobweb.silk.components.layout.numColumns
22 | import com.varabyte.kobweb.silk.style.CssStyle
23 | import com.varabyte.kobweb.silk.style.base
24 | import com.varabyte.kobweb.silk.style.toModifier
25 | import com.varabyte.kobweb.silk.theme.colors.palette.background
26 | import com.varabyte.kobweb.silk.theme.colors.palette.color
27 | import com.varabyte.kobweb.silk.theme.colors.palette.toPalette
28 | import io.github.opletter.css2kobweb.CodeBlock
29 | import io.github.opletter.css2kobweb.components.layouts.PageLayout
30 | import io.github.opletter.css2kobweb.components.widgets.KotlinCode
31 | import io.github.opletter.css2kobweb.css2kobwebAsCode
32 | import kotlinx.browser.window
33 | import kotlinx.coroutines.delay
34 | import org.jetbrains.compose.web.css.LineStyle
35 | import org.jetbrains.compose.web.css.cssRem
36 | import org.jetbrains.compose.web.css.fr
37 | import org.jetbrains.compose.web.css.px
38 | import org.jetbrains.compose.web.dom.H2
39 | import org.jetbrains.compose.web.dom.Text
40 | import org.jetbrains.compose.web.dom.TextArea
41 | import kotlin.time.Duration.Companion.milliseconds
42 |
43 | val TextAreaStyle = CssStyle.base {
44 | Modifier
45 | .fillMaxSize()
46 | .padding(topBottom = 0.5.cssRem, leftRight = 1.cssRem)
47 | .borderRadius(bottomLeft = 8.px, bottomRight = 8.px)
48 | .resize(Resize.None)
49 | .overflow { y(Overflow.Auto) }
50 | .overflowWrap(OverflowWrap.Normal)
51 | .styleModifier { property("tab-size", 4) }
52 | .backgroundColor(colorMode.toPalette().color)
53 | .color(colorMode.toPalette().background)
54 | }
55 |
56 | val TextAreaLabelBarStyle = CssStyle.base {
57 | Modifier
58 | .fillMaxWidth()
59 | .backgroundColor(Colors.Black)
60 | .padding(topBottom = 0.5.cssRem, leftRight = 1.cssRem)
61 | .color(Color.rgb(0x8bdbe2))
62 | .borderRadius(topLeft = 8.px, topRight = 8.px)
63 | }
64 |
65 | @Page
66 | @Composable
67 | fun HomePage() {
68 | var cssInput by remember { mutableStateOf("") }
69 | var outputCode: List by remember { mutableStateOf(emptyList()) }
70 |
71 | // get code here to avoid lagging onInput
72 | LaunchedEffect(cssInput) {
73 | try {
74 | if (cssInput.length > 5000) // debounce after semi-arbitrarily chosen number of characters
75 | delay(50.milliseconds)
76 | outputCode = if (cssInput.isNotBlank()) css2kobwebAsCode(cssInput) else emptyList()
77 | } catch (e: Exception) {
78 | // exceptions are expected while css is being typed / not invalid so only log them to verbose
79 | @Suppress("UNUSED_VARIABLE") val debug = e.stackTraceToString()
80 | js("console.debug(debug);") // why is console.debug not a thing?
81 | Unit
82 | }
83 | }
84 |
85 | PageLayout("CSS 2 Kobweb") {
86 | SimpleGrid(
87 | numColumns(1, md = 2),
88 | Modifier
89 | .fillMaxWidth()
90 | .flex(1)
91 | .gap(1.cssRem)
92 | .gridAutoRows { size(1.fr) }
93 | ) {
94 | Column(Modifier.fillMaxHeight()) {
95 | TextAreaHeader("CSS Input") {
96 | HeaderButton({ cssInput = "" }, Modifier.ariaLabel("clear text")) {
97 | FaTrashCan()
98 | }
99 | }
100 | TextArea(
101 | cssInput,
102 | TextAreaStyle.toModifier()
103 | .outlineStyle(LineStyle.None)
104 | .border { style(LineStyle.None) }
105 | .ariaLabel("CSS Input")
106 | .toAttrs {
107 | spellCheck(false)
108 | attr("data-enable-grammarly", "false")
109 | ref {
110 | it.focus()
111 | onDispose { }
112 | }
113 | onInput {
114 | cssInput = it.value
115 | }
116 | }
117 | )
118 | }
119 | // outer column needed for child's flex-grow (while keeping overflowY)
120 | Column(Modifier.overflow { x(Overflow.Auto) }) {
121 | Column(
122 | Modifier
123 | .fillMaxWidth()
124 | .height(0.px) // set to make overflow work
125 | .flexGrow(1)
126 | ) {
127 | TextAreaHeader("Kobweb Code") {
128 | CopyTextButton(outputCode.joinToString(""))
129 | }
130 | KotlinCode(outputCode, TextAreaStyle.toModifier())
131 | }
132 | }
133 | }
134 | }
135 | }
136 |
137 | @Composable
138 | fun TextAreaHeader(label: String, rightSideContent: @Composable () -> Unit) {
139 | Row(
140 | TextAreaLabelBarStyle.toModifier().columnGap(1.cssRem),
141 | verticalAlignment = Alignment.CenterVertically
142 | ) {
143 | H2(Modifier.fillMaxWidth().toAttrs()) {
144 | Text(label)
145 | }
146 |
147 | rightSideContent()
148 | }
149 | }
150 |
151 | @Composable
152 | fun HeaderButton(onClick: () -> Unit, modifier: Modifier = Modifier, content: @Composable () -> Unit) {
153 | Button({ onClick() }, modifier.fontSize(1.cssRem), size = ButtonSize.SM) {
154 | content()
155 | }
156 | }
157 |
158 | @Composable
159 | fun CopyTextButton(textToCopy: String) {
160 | var buttonText by remember { mutableStateOf("Copy") }
161 | HeaderButton(
162 | {
163 | window.navigator.clipboard.writeText(textToCopy)
164 | buttonText = "Copied!"
165 | window.setTimeout({ buttonText = "Copy" }, 2000)
166 | }
167 | ) { Text(buttonText) }
168 | }
--------------------------------------------------------------------------------
/site/src/jsMain/resources/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opLetter/css2kobweb/17386befdb5c76df2308fd484ba6a2dd3a006a40/site/src/jsMain/resources/public/favicon.ico
--------------------------------------------------------------------------------