From 6f0d90ab75cdcf1d4ba77ea1ce47c6c56b3e2aad Mon Sep 17 00:00:00 2001 From: Giorgio Garofalo Date: Mon, 4 Nov 2024 14:31:24 +0100 Subject: [PATCH] Refactor: use `StringCase` strategy for numbering strategies instead of one numbering per case --- .../numbering/NumberingCounterSymbol.kt | 39 ++++++------------- .../document/numbering/NumberingFormat.kt | 13 ++++--- .../eu/iamgio/quarkdown/util/StringCase.kt | 39 +++++++++++++++++++ .../kotlin/eu/iamgio/quarkdown/MiscTest.kt | 21 +++++++--- .../eu/iamgio/quarkdown/stdlib/String.kt | 8 ++-- 5 files changed, 78 insertions(+), 42 deletions(-) create mode 100644 core/src/main/kotlin/eu/iamgio/quarkdown/util/StringCase.kt diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/document/numbering/NumberingCounterSymbol.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/document/numbering/NumberingCounterSymbol.kt index 53906847..c8d0c575 100644 --- a/core/src/main/kotlin/eu/iamgio/quarkdown/document/numbering/NumberingCounterSymbol.kt +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/document/numbering/NumberingCounterSymbol.kt @@ -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 @@ -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 @@ -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() @@ -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() -} diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/document/numbering/NumberingFormat.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/document/numbering/NumberingFormat.kt index fdf9dbaa..a05bcbaa 100644 --- a/core/src/main/kotlin/eu/iamgio/quarkdown/document/numbering/NumberingFormat.kt +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/document/numbering/NumberingFormat.kt @@ -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, @@ -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 @@ -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) } } diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/util/StringCase.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/util/StringCase.kt new file mode 100644 index 00000000..c46aa482 --- /dev/null +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/util/StringCase.kt @@ -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) diff --git a/core/src/test/kotlin/eu/iamgio/quarkdown/MiscTest.kt b/core/src/test/kotlin/eu/iamgio/quarkdown/MiscTest.kt index 7800f95b..dcf7be4b 100644 --- a/core/src/test/kotlin/eu/iamgio/quarkdown/MiscTest.kt +++ b/core/src/test/kotlin/eu/iamgio/quarkdown/MiscTest.kt @@ -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 @@ -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 @@ -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") @@ -223,9 +223,18 @@ class MiscTest { assertEquals('.', (next() as NumberingFixedSymbol).value) assertIs(next()) assertEquals('.', (next() as NumberingFixedSymbol).value) - assertIs(next()) + + next().let { + assertIs(it) + assertEquals(StringCase.Lower, it.case) + } + assertEquals('-', (next() as NumberingFixedSymbol).value) - assertIs(next()) + + next().let { + assertIs(it) + assertEquals(StringCase.Upper, it.case) + } } fun format( diff --git a/stdlib/src/main/kotlin/eu/iamgio/quarkdown/stdlib/String.kt b/stdlib/src/main/kotlin/eu/iamgio/quarkdown/stdlib/String.kt index f62a1b44..98bafbd1 100644 --- a/stdlib/src/main/kotlin/eu/iamgio/quarkdown/stdlib/String.kt +++ b/stdlib/src/main/kotlin/eu/iamgio/quarkdown/stdlib/String.kt @@ -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. @@ -19,7 +21,7 @@ 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. @@ -27,7 +29,7 @@ fun uppercase(string: String) = string.uppercase().wrappedAsValue() * @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. @@ -35,4 +37,4 @@ fun lowercase(string: String) = string.lowercase().wrappedAsValue() * @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()