Skip to content

Commit

Permalink
Refactor: use StringCase strategy for numbering strategies instead …
Browse files Browse the repository at this point in the history
…of one numbering per case
  • Loading branch information
iamgio committed Nov 4, 2024
1 parent ac94f01 commit 6f0d90a
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package eu.iamgio.quarkdown.document.numbering

import com.github.fracpete.romannumerals4j.RomanNumeralFormat
import eu.iamgio.quarkdown.util.StringCase
import eu.iamgio.quarkdown.util.case

/**
* Represents a [NumberingSymbol] within a [NumberingFormat] with the responsibility of
Expand All @@ -13,7 +15,7 @@ interface NumberingCounterSymbol : NumberingSymbol {
* and an alternative strategy should be used.
* In the default [NumberingFormat.format] implementation,
* out-of-range values are simply mapped to their decimal representation.
* E.g. [UppercaseAlphaNumberingSymbol] can map values from 1-26 as `A`-`Z`. Value `0` is formatted as `0`.
* E.g. [AlphaNumberingSymbol] can map values from 1-26 as `A`-`Z`. Value `0` is formatted as `0`.
*/
val supportedRange: IntRange
get() = 0..Int.MAX_VALUE
Expand All @@ -34,29 +36,21 @@ data object DecimalNumberingSymbol : NumberingCounterSymbol {
}

/**
* A numbering strategy that counts items as uppercase letters of the latin alphabet: `0, A, B, C, ...`
* A numbering strategy that counts items as letters of the latin alphabet: `0, A, B, C, ...`
* @param case whether the letters should be uppercase or lowercase
*/
data object UppercaseAlphaNumberingSymbol : NumberingCounterSymbol {
data class AlphaNumberingSymbol(val case: StringCase) : NumberingCounterSymbol {
override val supportedRange: IntRange
get() = 1..'Z' - 'A' + 1

override fun map(index: Int) = ('A' + index - 1).toString()
override fun map(index: Int) = ('A' + index - 1).toString().case(case)
}

/**
* A numbering strategy that counts items as lowercase letters of the latin alphabet: `0, a, b, c, ...`
* A numbering strategy that counts items as Roman numerals: `0, I, II, III, ...`
* @param case whether the letters should be uppercase or lowercase
*/
data object LowercaseAlphaNumberingSymbol : NumberingCounterSymbol {
override val supportedRange: IntRange
get() = UppercaseAlphaNumberingSymbol.supportedRange

override fun map(index: Int) = UppercaseAlphaNumberingSymbol.map(index).lowercase()
}

/**
* A numbering strategy that counts items as uppercase roman numerals: `0, I, II, III, ...`
*/
data object UppercaseRomanNumberingSymbol : NumberingCounterSymbol {
data class RomanNumberingSymbol(val case: StringCase) : NumberingCounterSymbol {
// Provided by the romannumerals4j library: https://github.com/fracpete/romannumerals4j
private val format = RomanNumeralFormat()

Expand All @@ -65,16 +59,7 @@ data object UppercaseRomanNumberingSymbol : NumberingCounterSymbol {

override fun map(index: Int) =
index.let {
format.format(it) ?: throw IllegalStateException("Failed to format $it as a roman numeral")
format.format(it)?.case(case)
?: throw IllegalStateException("Failed to format $it as a roman numeral")
}
}

/**
* A numbering strategy that counts items as lowercase roman numerals: `0, i, ii, iii, ...`
*/
data object LowecaseRomanNumberingSymbol : NumberingCounterSymbol {
override val supportedRange: IntRange
get() = UppercaseRomanNumberingSymbol.supportedRange

override fun map(index: Int) = UppercaseRomanNumberingSymbol.map(index).lowercase()
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package eu.iamgio.quarkdown.document.numbering

import eu.iamgio.quarkdown.ast.attributes.SectionLocation
import eu.iamgio.quarkdown.document.numbering.NumberingFormat.Companion.fromString
import eu.iamgio.quarkdown.util.StringCase

/**
* Represents a format that defines how items (e.g. headings) are numbered in a document,
Expand All @@ -18,9 +19,9 @@ import eu.iamgio.quarkdown.document.numbering.NumberingFormat.Companion.fromStri
* In this example, the format consists of the following symbols:
* - `1` is a [DecimalNumberingSymbol], which counts `1, 2, 3, ...`
* - `.` is a [NumberingFixedSymbol]
* - `A` is an [UppercaseAlphaNumberingSymbol], which counts `A, B, C, ...`
* - `A` is an uppercase [AlphaNumberingSymbol], which counts `A, B, C, ...`
* - `.` is a [NumberingFixedSymbol]
* - `a` is a [LowercaseAlphaNumberingSymbol], which counts `a, b, c, ...`
* - `a` is a lowercase [AlphaNumberingSymbol], which counts `a, b, c, ...`
* A format can be imported and exported as a string via [fromString] and [format] respectively.
* @param symbols ordered list of symbols that define the format
* @see NumberingSymbol
Expand Down Expand Up @@ -116,10 +117,10 @@ data class NumberingFormat(
string.map {
when (it) {
'1' -> DecimalNumberingSymbol
'A' -> UppercaseAlphaNumberingSymbol
'a' -> LowercaseAlphaNumberingSymbol
'I' -> UppercaseRomanNumberingSymbol
'i' -> LowecaseRomanNumberingSymbol
'A' -> AlphaNumberingSymbol(StringCase.Upper)
'a' -> AlphaNumberingSymbol(StringCase.Lower)
'I' -> RomanNumberingSymbol(StringCase.Upper)
'i' -> RomanNumberingSymbol(StringCase.Lower)
else -> NumberingFixedSymbol(it)
}
}
Expand Down
39 changes: 39 additions & 0 deletions core/src/main/kotlin/eu/iamgio/quarkdown/util/StringCase.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package eu.iamgio.quarkdown.util

/**
* A general purpose strategy to transform a string to a specific case type.
*/
sealed interface StringCase {
/**
* Transforms a [string] according to the case.
* @return transformed string
*/
fun transform(string: String): String

/**
* Uppercase. `Hello` -> `HELLO`
*/
data object Upper : StringCase {
override fun transform(string: String) = string.uppercase()
}

/**
* Lowercase. `Hello` -> `hello`
*/
data object Lower : StringCase {
override fun transform(string: String) = string.lowercase()
}

/**
* Capitalize. `hello` -> `Hello`
*/
data object Capitalize : StringCase {
override fun transform(string: String) = string.replaceFirstChar(Char::titlecase)
}
}

/**
* Transforms [this] string to a specific case.
* @return the transformed string
*/
fun String.case(case: StringCase) = case.transform(this)
21 changes: 15 additions & 6 deletions core/src/test/kotlin/eu/iamgio/quarkdown/MiscTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ import eu.iamgio.quarkdown.ast.base.inline.Strong
import eu.iamgio.quarkdown.ast.base.inline.Text
import eu.iamgio.quarkdown.context.MutableContext
import eu.iamgio.quarkdown.context.toc.TableOfContents
import eu.iamgio.quarkdown.document.numbering.AlphaNumberingSymbol
import eu.iamgio.quarkdown.document.numbering.DecimalNumberingSymbol
import eu.iamgio.quarkdown.document.numbering.LowercaseAlphaNumberingSymbol
import eu.iamgio.quarkdown.document.numbering.NumberingFixedSymbol
import eu.iamgio.quarkdown.document.numbering.NumberingFormat
import eu.iamgio.quarkdown.document.numbering.UppercaseAlphaNumberingSymbol
import eu.iamgio.quarkdown.flavor.quarkdown.QuarkdownFlavor
import eu.iamgio.quarkdown.localization.LocaleLoader
import eu.iamgio.quarkdown.localization.LocaleNotSetException
Expand All @@ -28,6 +27,7 @@ import eu.iamgio.quarkdown.pipeline.output.OutputResourceGroup
import eu.iamgio.quarkdown.pipeline.output.TextOutputArtifact
import eu.iamgio.quarkdown.rendering.html.HtmlIdentifierProvider
import eu.iamgio.quarkdown.rendering.html.QuarkdownHtmlNodeRenderer
import eu.iamgio.quarkdown.util.StringCase
import java.nio.file.Files
import kotlin.test.Test
import kotlin.test.assertContentEquals
Expand Down Expand Up @@ -213,8 +213,8 @@ class MiscTest {
@Test
fun numbering() {
assertEquals("3", DecimalNumberingSymbol.map(3))
assertEquals("b", LowercaseAlphaNumberingSymbol.map(2))
assertEquals("C", UppercaseAlphaNumberingSymbol.map(3))
assertEquals("b", AlphaNumberingSymbol(StringCase.Lower).map(2))
assertEquals("C", AlphaNumberingSymbol(StringCase.Upper).map(3))

val format = NumberingFormat.fromString("1.1.a-A")

Expand All @@ -223,9 +223,18 @@ class MiscTest {
assertEquals('.', (next() as NumberingFixedSymbol).value)
assertIs<DecimalNumberingSymbol>(next())
assertEquals('.', (next() as NumberingFixedSymbol).value)
assertIs<LowercaseAlphaNumberingSymbol>(next())

next().let {
assertIs<AlphaNumberingSymbol>(it)
assertEquals(StringCase.Lower, it.case)
}

assertEquals('-', (next() as NumberingFixedSymbol).value)
assertIs<UppercaseAlphaNumberingSymbol>(next())

next().let {
assertIs<AlphaNumberingSymbol>(it)
assertEquals(StringCase.Upper, it.case)
}
}

fun format(
Expand Down
8 changes: 5 additions & 3 deletions stdlib/src/main/kotlin/eu/iamgio/quarkdown/stdlib/String.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package eu.iamgio.quarkdown.stdlib

import eu.iamgio.quarkdown.function.value.wrappedAsValue
import eu.iamgio.quarkdown.util.StringCase
import eu.iamgio.quarkdown.util.case

/**
* `String` stdlib module exporter.
Expand All @@ -19,20 +21,20 @@ val String: Module =
* @param string string to convert
* @return a new uppercase string
*/
fun uppercase(string: String) = string.uppercase().wrappedAsValue()
fun uppercase(string: String) = string.case(StringCase.Upper).wrappedAsValue()

/**
* Converts a string to lowercase.
* Example: `Hello, World!` -> `hello, world!`
* @param string string to convert
* @return a new lowercase string
*/
fun lowercase(string: String) = string.lowercase().wrappedAsValue()
fun lowercase(string: String) = string.case(StringCase.Lower).wrappedAsValue()

/**
* Capitalizes the first character of a string.
* Example: `hello, world!` -> `Hello, world!`
* @param string string to capitalize
* @return a new string with the first character capitalized
*/
fun capitalize(string: String) = string.replaceFirstChar(Char::titlecase).wrappedAsValue()
fun capitalize(string: String) = string.case(StringCase.Capitalize).wrappedAsValue()

0 comments on commit 6f0d90a

Please sign in to comment.