Skip to content

Commit

Permalink
Initial implementation of Kotlin/Native support.
Browse files Browse the repository at this point in the history
Includes all features except PDF generation and puzzle cell images.
Builds a placeholder binary for Linux, Windows, and macOS (all X64). In
the future, this binary will be a command line utility exposing useful
Kotwords features for dumping data, conversions, etc.

The GitHub CI build should now run on Mac/Windows, solely for the native
targets of those platforms. JS/JVM builds continue to be exclusive to
Linux for CI purposes.

Implementation notes:

- Zip files use korio. This is technically a multiplatform solution, but
newer versions of the library were merged into a much larger game engine
package, Korge, and are not currently available as a standalone package.
Perhaps we can use this across platforms if this is resolved.

- HTML/XML parsing uses ksoup. This is a multiplatform solution which is
effectively a drop-in replacement for jsoup. It may be suitable for all
platforms as it matures, if the size/speed overhead on JS/JVM is
minimal.

- ksoup currently depends on kotlinx-datetime 0.5.0 which seems to cause
our build to fail if we depend on 0.6.0-RC.2 (which is useful for
replacing klock). For now we can continue to hold off.

- URL encoding uses a small standalone library which could be usable on
all platforms for simplicity.
  • Loading branch information
jpd236 committed Mar 27, 2024
1 parent 127b0dc commit 7f64528
Show file tree
Hide file tree
Showing 54 changed files with 495 additions and 125 deletions.
44 changes: 33 additions & 11 deletions .github/workflows/gradle-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,58 @@ on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
distribution: 'microsoft'
java-version: '17'
- name: Cache Gradle packages
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
restore-keys: ${{ runner.os }}-gradle-
- name: Cache Kotlin Native
uses: actions/cache@v4
with:
path: ~/.konan
key: ${{ runner.os }}-konan-${{ hashFiles('**/*.gradle.kts') }}
restore-keys: ${{ runner.os }}-konan-
- name: Build with Gradle
run: ./gradlew --no-daemon assemble check
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
./gradlew --no-daemon jvmJar jsBrowserDistribution linkReleaseExecutableLinuxX64 jvmTest jsTest linuxX64Test
elif [ "$RUNNER_OS" == "macOS" ]; then
./gradlew --no-daemon linkReleaseExecutableMacosX64 macosX64Test
elif [ "$RUNNER_OS" == "Windows" ]; then
./gradlew --no-daemon linkReleaseExecutableMingwX64 mingwX64Test
else
echo "Unknown OS: $RUNNER_OS"
exit 1
fi
shell: bash
- name: Publish snapshot
if: runner.os == 'Linux'
env:
OSSRH_DEPLOY_USERNAME: ${{ secrets.OSSRH_DEPLOY_USERNAME }}
OSSRH_DEPLOY_PASSWORD: ${{ secrets.OSSRH_DEPLOY_PASSWORD }}
PGP_KEY: ${{ secrets.PGP_KEY }}
PGP_KEY_ID: ${{ secrets.PGP_KEY_ID }}
PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
run: ./publish-snapshot.sh
- name: Archive browser distribution zip
uses: actions/upload-artifact@v3
- name: Archive distributed artifacts
uses: actions/upload-artifact@v4
with:
name: browser-distribution
path: build/zip/kotwords-browser-distribution-*.zip
name: kotwords-build-${{ runner.os }}
path: |
build/zip/kotwords-browser-distribution-*.zip
build/bin/**/releaseExecutable/kotwords.*
39 changes: 31 additions & 8 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack

plugins {
Expand Down Expand Up @@ -28,6 +30,20 @@ kotlin {
binaries.executable()
}

mingwX64()
linuxX64()
macosX64()

targets.all {
if (this is KotlinNativeTarget) {
binaries.executable {
entryPoint = "com.jeffpdavidson.kotwords.cli.main"
}
}
}

applyDefaultHierarchyTemplate()

sourceSets {
all {
languageSettings {
Expand All @@ -47,7 +63,8 @@ kotlin {
implementation("com.github.ajalt.colormath:colormath:3.3.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")

// TODO: Migrate to kotlinx-datetime if parsing/formatting support is added.
// TODO: Migrate to kotlinx-datetime when it can be done without breaking ksoup.
// Ensure any size hit to the JS bundle is acceptable.
implementation("com.soywiz.korlibs.klock:klock:4.0.10")
}
}
Expand All @@ -57,6 +74,7 @@ kotlin {
implementation("org.jetbrains.kotlin:kotlin-test-annotations-common")
implementation("org.jetbrains.kotlin:kotlin-test-common")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
implementation("io.github.pdvrieze.xmlutil:testutil:0.86.3")
}

languageSettings {
Expand Down Expand Up @@ -102,24 +120,29 @@ kotlin {
optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
}
}
}

targets.configureEach {
compilations.configureEach {
compilerOptions.configure {
freeCompilerArgs.add("-Xexpect-actual-classes")
val nativeMain by getting {
dependencies {
implementation("com.soywiz.korlibs.korio:korio:4.0.10")
implementation("com.fleeksoft.ksoup:ksoup:0.1.2")
implementation("net.thauvin.erik.urlencoder:urlencoder-lib:1.4.0")
}
}
}

@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
freeCompilerArgs.add("-Xexpect-actual-classes")
}
}

tasks {
// Omit .web package from documentation
// Omit .web and .cli package from documentation
dokkaHtml.configure {
dokkaSourceSets {
configureEach {
perPackageOption {
matchingRegex.set("""com.jeffpdavidson\.kotwords\.web.*""")
matchingRegex.set("""com.jeffpdavidson\.kotwords\.(?:web|cli).*""")
suppress.set(true)
}
}
Expand Down
3 changes: 2 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
org.gradle.jvmargs=-Xmx2000m
org.gradle.jvmargs=-Xmx2000m
kotlin.native.ignoreDisabledTargets=true
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,15 @@ class AcrossLite(private val binaryData: ByteArray) : DelegatingPuzzleable() {
// Skip the null terminator.
skip(1)
}

"RTBL" -> {
val data = readNullTerminatedString(Charset.CP_1252)
data.split(';').filterNot { it.isEmpty() }.forEach {
val parts = it.split(':')
rebusTable[parts[0].trim().toInt()] = parts[1]
}
}

"RUSR" -> {
// Handle invalid/empty RUSR sections gracefully.
if (sectionLength == 0.toShort()) {
Expand All @@ -152,6 +154,7 @@ class AcrossLite(private val binaryData: ByteArray) : DelegatingPuzzleable() {
}
}
}

"GEXT" -> {
for (y in 0 until height) {
for (x in 0 until width) {
Expand All @@ -164,6 +167,7 @@ class AcrossLite(private val binaryData: ByteArray) : DelegatingPuzzleable() {
// Skip the null terminator.
skip(1)
}

else -> {
// Skip section + null terminator.
skip(sectionLength + 1L)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,55 @@ internal expect object Encodings {
fun decodeHtmlEntities(string: String): String

fun unescape(string: String): String
}

/** Unicode values corresponding to bytes 0x80-0x9F in CP1252. */
private val CP1252_DECODE_TABLE = listOf(
'\u20ac', '\u0081', '\u201a', '\u0192', '\u201e', '\u2026', '\u2020', '\u2021',
'\u02c6', '\u2030', '\u0160', '\u2039', '\u0152', '\u008d', '\u017d', '\u008f',
'\u0090', '\u2018', '\u2019', '\u201c', '\u201d', '\u2022', '\u2013', '\u2014',
'\u02dc', '\u2122', '\u0161', '\u203a', '\u0153', '\u009d', '\u017e', '\u0178',
)

/** Map from supported CP1252 unicode values to their byte value in CP1252. */
private val CP1252_ENCODE_TABLE = CP1252_DECODE_TABLE.withIndex().associate { it.value to it.index }

internal fun Encodings.commonDecodeCp1252(bytes: ByteArray): String {
return bytes.map { byte ->
val intValue = byte.toInt() and 0xFF
if (intValue in 0x00..0x7F || intValue in 0xA0..0xFF) {
// ISO-8859-1 characters map directly.
intValue.toChar()
} else {
// Between 0x80 and 0x9F - look up the value in the table.
CP1252_DECODE_TABLE[intValue - 0x80]
}
}.joinToString("")
}

internal fun Encodings.commonEncodeCp1252(string: String): ByteArray {
return string.map { ch ->
if (ch.code in 0x00..0x7F || ch.code in 0xA0..0xFF) {
// ISO-8859-1 characters map directly.
ch.code.toByte()
} else {
val cp1252Byte = CP1252_ENCODE_TABLE[ch]
if (cp1252Byte != null) {
// One of the supported CP1252 characters.
(0x80 + cp1252Byte).toByte()
} else {
// Unsupported value - use default '?' character.
'?'.code.toByte()
}
}
}.toByteArray()
}

private val UNESCAPE_PATTERN = "%u([0-9A-Fa-f]{4})|%([0-9A-Fa-f]{2})".toRegex()

internal fun Encodings.commonUnescape(string: String): String {
return UNESCAPE_PATTERN.replace(string) { result ->
val code = result.groupValues[1].ifEmpty { result.groupValues[2] }
code.toInt(16).toChar().toString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class Ipuz(private val json: String) : Puzzleable() {
is IpuzJson.StyleRef -> {
ipuz.styles.getOrElse(style.style) { IpuzJson.StyleSpec() }
}

is IpuzJson.StyleSpec -> style
}
val cellType = when {
Expand Down Expand Up @@ -483,6 +484,7 @@ internal data class IpuzJson @OptIn(ExperimentalSerializationApi::class) constru
put("number", element[0].jsonPrimitive)
put("clue", element[1].jsonPrimitive)
}

is JsonObject -> element
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
import nl.adaptivity.xmlutil.XmlDeclMode
import nl.adaptivity.xmlutil.XmlException
import nl.adaptivity.xmlutil.core.XmlVersion
import nl.adaptivity.xmlutil.serialization.XML
import nl.adaptivity.xmlutil.serialization.XmlElement
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ object Pdf {
val backgroundColor = when {
square.backgroundColor.isNotBlank() ->
getAdjustedColor(RGB(square.backgroundColor), blackSquareLightnessAdjustment)

square.cellType == Puzzle.CellType.BLOCK && !puzzle.diagramless -> gridBlackColor
else -> RGB("#ffffff")
}
Expand Down Expand Up @@ -492,6 +493,7 @@ object Pdf {
)
}
}

is TextNode -> {
val currentFont = getFont(fontFamily, nodeState.boldTagLevel, nodeState.italicTagLevel)
val currentScript = getScript(nodeState.subTagLevel, nodeState.supTagLevel)
Expand Down Expand Up @@ -727,6 +729,7 @@ object Pdf {
currentLinePosition += getTextWidth(element.text, currentFont, currentFontSize)
}
}

is RichTextElement.NewLine -> {
val offset = (if (i == lastNewLineIndex) nextFontSize else fontSize) * LINE_SPACING
if (render) {
Expand All @@ -736,6 +739,7 @@ object Pdf {
}
positionY -= offset
}

is RichTextElement.SetFormat -> {
if (render) {
val newFont = element.format.font
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class PuzzleMe(val json: String) : DelegatingPuzzleable() {
data.cellInfos.find { it.isCircled } != null -> {
cellInfoMap.filterValues { it.isCircled }.keys
}

else -> {
data.backgroundShapeBoxes.filter { it.size == 2 }.map { it[0] to it[1] }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.jeffpdavidson.kotwords.formats.json.xwordinfo

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

internal object XWordInfoAcrosticJson {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ data class Acrostic(
quoteWord.add(Puzzle.Coordinate(x = x, y = y))
cell
}

' ' -> Puzzle.Cell(cellType = Puzzle.CellType.BLOCK)
else -> {
// Replace hyphen with en-dash for aesthetics.
Expand Down Expand Up @@ -250,6 +251,7 @@ data class Acrostic(
in 'A'..'Z' -> {
solutionWords.add(ch to wordIndex)
}

' ' -> wordIndex++
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package com.jeffpdavidson.kotwords.model

import com.jeffpdavidson.kotwords.formats.Puzzleable
import kotlinx.serialization.Serializable
import kotlin.math.floor
import kotlin.math.roundToInt

@Serializable
data class HeartsAndArrows(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,11 @@ data class RowsGarden(
}
letters.chunked(3).joinToString(separator = "...", prefix = "...", postfix = "...")
}

validWidthLong -> {
letters.chunked(3).joinToString(separator = "...")
}

else -> {
throw IllegalArgumentException("Outer row length must be $validWidthShort or $validWidthLong")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,18 +112,21 @@ data class SpellWeaving(
}
return position.copy(x = position.x + 1, direction = Direction.RIGHT)
}

Direction.DOWN -> {
if (isPointInGrid(position.x, position.y + 1, middleRowLength)) {
return position.copy(y = position.y + 1)
}
return position.copy(x = position.x - 1, direction = Direction.LEFT)
}

Direction.RIGHT -> {
if (isPointInGrid(position.x + 1, position.y, middleRowLength)) {
return position.copy(x = position.x + 1)
}
return position.copy(y = position.y + 1, direction = Direction.DOWN)
}

Direction.LEFT -> {
if (isPointInGrid(position.x - 1, position.y, middleRowLength)) {
return position.copy(x = position.x - 1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ import kotlin.reflect.KClass
expect suspend fun <T : Any> readBinaryResource(clazz: KClass<T>, resourceName: String): ByteArray

/** Read the given resource as a String. */
expect suspend fun <T : Any> readStringResource(clazz: KClass<T>, resourceName: String): String
expect suspend fun <T : Any> readStringResource(clazz: KClass<T>, resourceName: String): String

/** Annotation to skip a test for native targets. */
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
expect annotation class IgnoreNative()
Loading

0 comments on commit 7f64528

Please sign in to comment.