From 7d5af4e83f5b53274718797d0b6d94c087db117c Mon Sep 17 00:00:00 2001 From: Dorian Grolaux <6209874+MrSkwiggs@users.noreply.github.com> Date: Tue, 28 Jan 2025 15:55:03 +0100 Subject: [PATCH 1/7] Fix number formatting --- Sources/Ignite/Modifiers/Opacity.swift | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Sources/Ignite/Modifiers/Opacity.swift b/Sources/Ignite/Modifiers/Opacity.swift index c72f2bfd..6ee75109 100644 --- a/Sources/Ignite/Modifiers/Opacity.swift +++ b/Sources/Ignite/Modifiers/Opacity.swift @@ -5,6 +5,8 @@ // See LICENSE for license information. // +import Foundation + /// A modifier that applies opacity styling to HTML elements struct OpacityModifier: HTMLModifier { /// The opacity value between 0% (transparent) and 100% (opaque) @@ -31,12 +33,17 @@ struct OpacityModifier: HTMLModifier { /// - Parameter content: The HTML element to modify /// - Returns: The modified HTML with opacity applied func body(content: some HTML) -> any HTML { + let formatter = NumberFormatter() + formatter.locale = Locale(identifier: "en-US") + formatter.maximumFractionDigits = 1 if let percentage, percentage != 100% { - content.style(.opacity, percentage.value.formatted()) + let formattedNumber = formatter.string(for: percentage) ?? percentage.value.formatted() + return content.style(.opacity, formattedNumber) } else if let doubleValue, doubleValue != 1 { - content.style(.opacity, doubleValue.formatted()) + let formattedNumber = formatter.string(for: doubleValue) ?? doubleValue.formatted() + return content.style(.opacity, formattedNumber) } - content + return content } } From 35ad5cb48d29cae3ac3635d4db51b0e16213a723 Mon Sep 17 00:00:00 2001 From: Dorian Grolaux <6209874+MrSkwiggs@users.noreply.github.com> Date: Tue, 28 Jan 2025 16:05:41 +0100 Subject: [PATCH 2/7] Use NumberFormatter & refactor sites --- Sources/Ignite/Elements/Slide.swift | 2 +- .../Ignite/Framework/NumberFormatter.swift | 30 +++++++++++++++++++ Sources/Ignite/Modifiers/LineSpacing.swift | 4 +-- Sources/Ignite/Modifiers/Opacity.swift | 11 ++----- 4 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 Sources/Ignite/Framework/NumberFormatter.swift diff --git a/Sources/Ignite/Elements/Slide.swift b/Sources/Ignite/Elements/Slide.swift index ca0f5092..f5149308 100644 --- a/Sources/Ignite/Elements/Slide.swift +++ b/Sources/Ignite/Elements/Slide.swift @@ -72,7 +72,7 @@ public struct Slide: BlockHTML { .style( .init(.height, value: "100%"), .init(.objectFit, value: "cover"), - .init(.opacity, value: backgroundOpacity.formatted()) + .init(.opacity, value: NumberFormatter.format(backgroundOpacity)) ) } diff --git a/Sources/Ignite/Framework/NumberFormatter.swift b/Sources/Ignite/Framework/NumberFormatter.swift new file mode 100644 index 00000000..7e66cf8b --- /dev/null +++ b/Sources/Ignite/Framework/NumberFormatter.swift @@ -0,0 +1,30 @@ +// +// NumberFormatter.swift +// Ignite +// https://www.github.com/twostraws/Ignite +// See LICENSE for license information. +// + +import Foundation + +internal struct NumberFormatter { + private static let doubleFormatter: Foundation.NumberFormatter = { + let formatter = Foundation.NumberFormatter() + formatter.numberStyle = .decimal + formatter.locale = .init(identifier: "en_US") + formatter.maximumFractionDigits = 1 + return formatter + }() + + static func format(_ value: Double) -> String { + doubleFormatter.string(for: value) ?? value.formatted() + } + + static func format(_ value: Percentage) -> String { + doubleFormatter.string(for: value) ?? value.value.formatted() + } + + static func format(_ value: Float) -> String { + doubleFormatter.string(for: value) ?? value.formatted() + } +} diff --git a/Sources/Ignite/Modifiers/LineSpacing.swift b/Sources/Ignite/Modifiers/LineSpacing.swift index bd19b75a..a0585b08 100644 --- a/Sources/Ignite/Modifiers/LineSpacing.swift +++ b/Sources/Ignite/Modifiers/LineSpacing.swift @@ -28,13 +28,13 @@ struct LineSpacingModifier: HTMLModifier { func body(content: some HTML) -> any HTML { if content.body.isComposite { if let customHeight { - content.containerStyle(.init(.lineHeight, value: customHeight.formatted())) + content.containerStyle(.init(.lineHeight, value: NumberFormatter.format(customHeight))) } else if let presetHeight { content.containerClass("lh-\(presetHeight.rawValue)") } } else { if let customHeight { - content.style(.init(.lineHeight, value: customHeight.formatted())) + content.style(.init(.lineHeight, value: NumberFormatter.format(customHeight))) } else if let presetHeight { content.class("lh-\(presetHeight.rawValue)") } diff --git a/Sources/Ignite/Modifiers/Opacity.swift b/Sources/Ignite/Modifiers/Opacity.swift index 6ee75109..fcb67ebb 100644 --- a/Sources/Ignite/Modifiers/Opacity.swift +++ b/Sources/Ignite/Modifiers/Opacity.swift @@ -33,17 +33,12 @@ struct OpacityModifier: HTMLModifier { /// - Parameter content: The HTML element to modify /// - Returns: The modified HTML with opacity applied func body(content: some HTML) -> any HTML { - let formatter = NumberFormatter() - formatter.locale = Locale(identifier: "en-US") - formatter.maximumFractionDigits = 1 if let percentage, percentage != 100% { - let formattedNumber = formatter.string(for: percentage) ?? percentage.value.formatted() - return content.style(.opacity, formattedNumber) + content.style(.opacity, NumberFormatter.format(percentage)) } else if let doubleValue, doubleValue != 1 { - let formattedNumber = formatter.string(for: doubleValue) ?? doubleValue.formatted() - return content.style(.opacity, formattedNumber) + content.style(.opacity, NumberFormatter.format(doubleValue)) } - return content + content } } From 6741faae33ffe22085c8485404ffea72e68a71ab Mon Sep 17 00:00:00 2001 From: Dorian Grolaux <6209874+MrSkwiggs@users.noreply.github.com> Date: Tue, 28 Jan 2025 16:07:32 +0100 Subject: [PATCH 3/7] Add documentation --- Sources/Ignite/Framework/NumberFormatter.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/Ignite/Framework/NumberFormatter.swift b/Sources/Ignite/Framework/NumberFormatter.swift index 7e66cf8b..c7e80f73 100644 --- a/Sources/Ignite/Framework/NumberFormatter.swift +++ b/Sources/Ignite/Framework/NumberFormatter.swift @@ -8,6 +8,7 @@ import Foundation internal struct NumberFormatter { + /// A number formatter that formats numbers with a single decimal, using the `.` separator. Locale-independent. private static let doubleFormatter: Foundation.NumberFormatter = { let formatter = Foundation.NumberFormatter() formatter.numberStyle = .decimal @@ -16,14 +17,17 @@ internal struct NumberFormatter { return formatter }() + /// Formats a double value as a string. Locale-independent. static func format(_ value: Double) -> String { doubleFormatter.string(for: value) ?? value.formatted() } + /// Formats a percentage value as a string. Locale-independent. static func format(_ value: Percentage) -> String { doubleFormatter.string(for: value) ?? value.value.formatted() } + /// Formats a float value as a string. Locale-independent static func format(_ value: Float) -> String { doubleFormatter.string(for: value) ?? value.formatted() } From 50567ca731df9d7419354f546161dfaa9f31266c Mon Sep 17 00:00:00 2001 From: Dorian Grolaux <6209874+MrSkwiggs@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:35:52 +0100 Subject: [PATCH 4/7] Refactor using FormatStyle extensions --- Sources/Ignite/Elements/Slide.swift | 2 +- .../FormatStyle-NonLocalizedDecimal.swift | 26 ++++++++++++++ .../Ignite/Framework/NumberFormatter.swift | 34 ------------------- Sources/Ignite/Modifiers/LineSpacing.swift | 4 +-- Sources/Ignite/Modifiers/Opacity.swift | 4 +-- 5 files changed, 31 insertions(+), 39 deletions(-) create mode 100644 Sources/Ignite/Extensions/FormatStyle-NonLocalizedDecimal.swift delete mode 100644 Sources/Ignite/Framework/NumberFormatter.swift diff --git a/Sources/Ignite/Elements/Slide.swift b/Sources/Ignite/Elements/Slide.swift index f5149308..e473f81c 100644 --- a/Sources/Ignite/Elements/Slide.swift +++ b/Sources/Ignite/Elements/Slide.swift @@ -72,7 +72,7 @@ public struct Slide: BlockHTML { .style( .init(.height, value: "100%"), .init(.objectFit, value: "cover"), - .init(.opacity, value: NumberFormatter.format(backgroundOpacity)) + .init(.opacity, value: backgroundOpacity.formatted(.nonLocalizedDecimal)) ) } diff --git a/Sources/Ignite/Extensions/FormatStyle-NonLocalizedDecimal.swift b/Sources/Ignite/Extensions/FormatStyle-NonLocalizedDecimal.swift new file mode 100644 index 00000000..fd05feca --- /dev/null +++ b/Sources/Ignite/Extensions/FormatStyle-NonLocalizedDecimal.swift @@ -0,0 +1,26 @@ +// +// FormatStyle-NonLocalizedDecimal.swift +// Ignite +// https://www.github.com/twostraws/Ignite +// See LICENSE for license information. +// + +import Foundation + +extension FormatStyle where Self == FloatingPointFormatStyle, FormatInput == Double { + /// A format style that displays a floating point number with one decimal place, enforcing the use of a `.` as the decimal separator. + static var nonLocalizedDecimal: Self { + FloatingPointFormatStyle() + .precision(.fractionLength(1)) + .locale(Locale(identifier: "en_US")) + } +} + +extension FormatStyle where Self == FloatingPointFormatStyle, FormatInput == Float { + /// A format style that displays a floating point number with one decimal place, enforcing the use of a `.` as the decimal separator. + static var nonLocalizedDecimal: Self { + FloatingPointFormatStyle() + .precision(.fractionLength(1)) + .locale(Locale(identifier: "en_US")) + } +} diff --git a/Sources/Ignite/Framework/NumberFormatter.swift b/Sources/Ignite/Framework/NumberFormatter.swift deleted file mode 100644 index c7e80f73..00000000 --- a/Sources/Ignite/Framework/NumberFormatter.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// NumberFormatter.swift -// Ignite -// https://www.github.com/twostraws/Ignite -// See LICENSE for license information. -// - -import Foundation - -internal struct NumberFormatter { - /// A number formatter that formats numbers with a single decimal, using the `.` separator. Locale-independent. - private static let doubleFormatter: Foundation.NumberFormatter = { - let formatter = Foundation.NumberFormatter() - formatter.numberStyle = .decimal - formatter.locale = .init(identifier: "en_US") - formatter.maximumFractionDigits = 1 - return formatter - }() - - /// Formats a double value as a string. Locale-independent. - static func format(_ value: Double) -> String { - doubleFormatter.string(for: value) ?? value.formatted() - } - - /// Formats a percentage value as a string. Locale-independent. - static func format(_ value: Percentage) -> String { - doubleFormatter.string(for: value) ?? value.value.formatted() - } - - /// Formats a float value as a string. Locale-independent - static func format(_ value: Float) -> String { - doubleFormatter.string(for: value) ?? value.formatted() - } -} diff --git a/Sources/Ignite/Modifiers/LineSpacing.swift b/Sources/Ignite/Modifiers/LineSpacing.swift index a0585b08..beb73f81 100644 --- a/Sources/Ignite/Modifiers/LineSpacing.swift +++ b/Sources/Ignite/Modifiers/LineSpacing.swift @@ -28,13 +28,13 @@ struct LineSpacingModifier: HTMLModifier { func body(content: some HTML) -> any HTML { if content.body.isComposite { if let customHeight { - content.containerStyle(.init(.lineHeight, value: NumberFormatter.format(customHeight))) + content.containerStyle(.init(.lineHeight, value: customHeight.formatted(.nonLocalizedDecimal))) } else if let presetHeight { content.containerClass("lh-\(presetHeight.rawValue)") } } else { if let customHeight { - content.style(.init(.lineHeight, value: NumberFormatter.format(customHeight))) + content.style(.init(.lineHeight, value: customHeight.formatted(.nonLocalizedDecimal))) } else if let presetHeight { content.class("lh-\(presetHeight.rawValue)") } diff --git a/Sources/Ignite/Modifiers/Opacity.swift b/Sources/Ignite/Modifiers/Opacity.swift index fcb67ebb..5b7b3b78 100644 --- a/Sources/Ignite/Modifiers/Opacity.swift +++ b/Sources/Ignite/Modifiers/Opacity.swift @@ -34,9 +34,9 @@ struct OpacityModifier: HTMLModifier { /// - Returns: The modified HTML with opacity applied func body(content: some HTML) -> any HTML { if let percentage, percentage != 100% { - content.style(.opacity, NumberFormatter.format(percentage)) + content.style(.opacity, percentage.value.formatted(.nonLocalizedDecimal)) } else if let doubleValue, doubleValue != 1 { - content.style(.opacity, NumberFormatter.format(doubleValue)) + content.style(.opacity, doubleValue.formatted(.nonLocalizedDecimal)) } content } From 2dfa13b150b9ebfbb9848c02be5098b437dc2e43 Mon Sep 17 00:00:00 2001 From: Dorian Grolaux <6209874+MrSkwiggs@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:51:57 +0100 Subject: [PATCH 5/7] Add more tests and improve format style logic --- .../FormatStyle-NonLocalizedDecimal.swift | 22 +++++++++++++++---- Sources/Ignite/Modifiers/Opacity.swift | 4 ++-- Tests/IgniteTesting/Modifiers/Opacity.swift | 22 +++++++++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/Sources/Ignite/Extensions/FormatStyle-NonLocalizedDecimal.swift b/Sources/Ignite/Extensions/FormatStyle-NonLocalizedDecimal.swift index fd05feca..c210a828 100644 --- a/Sources/Ignite/Extensions/FormatStyle-NonLocalizedDecimal.swift +++ b/Sources/Ignite/Extensions/FormatStyle-NonLocalizedDecimal.swift @@ -10,8 +10,15 @@ import Foundation extension FormatStyle where Self == FloatingPointFormatStyle, FormatInput == Double { /// A format style that displays a floating point number with one decimal place, enforcing the use of a `.` as the decimal separator. static var nonLocalizedDecimal: Self { - FloatingPointFormatStyle() - .precision(.fractionLength(1)) + nonLocalizedDecimal(decimalPlaces: 1) + } + + /// A format style that displays a floating point number enforcing the use of a `.` as the decimal separator. + /// - Parameter decimalPlaces: The number of decimal places to display. Defaults to 1. + static func nonLocalizedDecimal(decimalPlaces: Int = 1) -> Self { + let precision = max(0, decimalPlaces) + return FloatingPointFormatStyle() + .precision(.fractionLength(0...precision)) .locale(Locale(identifier: "en_US")) } } @@ -19,8 +26,15 @@ extension FormatStyle where Self == FloatingPointFormatStyle, FormatInpu extension FormatStyle where Self == FloatingPointFormatStyle, FormatInput == Float { /// A format style that displays a floating point number with one decimal place, enforcing the use of a `.` as the decimal separator. static var nonLocalizedDecimal: Self { - FloatingPointFormatStyle() - .precision(.fractionLength(1)) + nonLocalizedDecimal(decimalPlaces: 1) + } + + /// A format style that displays a floating point number enforcing the use of a `.` as the decimal separator. + /// - Parameter decimalPlaces: The number of decimal places to display. Defaults to 1. + static func nonLocalizedDecimal(decimalPlaces: Int = 1) -> Self { + let precision = max(0, decimalPlaces) + return FloatingPointFormatStyle() + .precision(.fractionLength(0...precision)) .locale(Locale(identifier: "en_US")) } } diff --git a/Sources/Ignite/Modifiers/Opacity.swift b/Sources/Ignite/Modifiers/Opacity.swift index 5b7b3b78..a0fdd941 100644 --- a/Sources/Ignite/Modifiers/Opacity.swift +++ b/Sources/Ignite/Modifiers/Opacity.swift @@ -34,9 +34,9 @@ struct OpacityModifier: HTMLModifier { /// - Returns: The modified HTML with opacity applied func body(content: some HTML) -> any HTML { if let percentage, percentage != 100% { - content.style(.opacity, percentage.value.formatted(.nonLocalizedDecimal)) + content.style(.opacity, percentage.value.formatted(.nonLocalizedDecimal(decimalPlaces: 3))) } else if let doubleValue, doubleValue != 1 { - content.style(.opacity, doubleValue.formatted(.nonLocalizedDecimal)) + content.style(.opacity, doubleValue.formatted(.nonLocalizedDecimal(decimalPlaces: 3))) } content } diff --git a/Tests/IgniteTesting/Modifiers/Opacity.swift b/Tests/IgniteTesting/Modifiers/Opacity.swift index 43657183..6b0156d5 100644 --- a/Tests/IgniteTesting/Modifiers/Opacity.swift +++ b/Tests/IgniteTesting/Modifiers/Opacity.swift @@ -33,4 +33,26 @@ struct OpacityTests { #expect(output == "\"\(image.description)\"") } + + @Test("Checks that the opacity value is correctly formatted", arguments: [ + (value: 0.123456, expected: "0.123"), + (value: 0.15, expected: "0.15"), + (value: 0.1, expected: "0.1"), + (value: 0.45678, expected: "0.457"), + (value: 0, expected: "0") + ]) + func opacityFormatting(testCase: (value: Double, expected: String)) async throws { + let element = Text("Test").opacity(testCase.value) + let output = element.render() + + #expect(output == "

Test

") + } + + @Test("Checks that full opacity is not rendered") + func fullOpacity() async throws { + let element = Text("Test").opacity(1) + let output = element.render() + + #expect(output == "

Test

") + } } From 157829f4b8592a1bc4b8898b35b54c8b302ec3c0 Mon Sep 17 00:00:00 2001 From: Dorian Grolaux <6209874+MrSkwiggs@users.noreply.github.com> Date: Wed, 29 Jan 2025 15:39:49 +0100 Subject: [PATCH 6/7] Use places as param name --- .../FormatStyle-NonLocalizedDecimal.swift | 16 ++++++++-------- Sources/Ignite/Modifiers/Opacity.swift | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/Ignite/Extensions/FormatStyle-NonLocalizedDecimal.swift b/Sources/Ignite/Extensions/FormatStyle-NonLocalizedDecimal.swift index c210a828..d472968a 100644 --- a/Sources/Ignite/Extensions/FormatStyle-NonLocalizedDecimal.swift +++ b/Sources/Ignite/Extensions/FormatStyle-NonLocalizedDecimal.swift @@ -10,13 +10,13 @@ import Foundation extension FormatStyle where Self == FloatingPointFormatStyle, FormatInput == Double { /// A format style that displays a floating point number with one decimal place, enforcing the use of a `.` as the decimal separator. static var nonLocalizedDecimal: Self { - nonLocalizedDecimal(decimalPlaces: 1) + nonLocalizedDecimal(places: 1) } /// A format style that displays a floating point number enforcing the use of a `.` as the decimal separator. - /// - Parameter decimalPlaces: The number of decimal places to display. Defaults to 1. - static func nonLocalizedDecimal(decimalPlaces: Int = 1) -> Self { - let precision = max(0, decimalPlaces) + /// - Parameter places: The number of decimal places to display. Defaults to 1. + static func nonLocalizedDecimal(places: Int = 1) -> Self { + let precision = max(0, places) return FloatingPointFormatStyle() .precision(.fractionLength(0...precision)) .locale(Locale(identifier: "en_US")) @@ -26,13 +26,13 @@ extension FormatStyle where Self == FloatingPointFormatStyle, FormatInpu extension FormatStyle where Self == FloatingPointFormatStyle, FormatInput == Float { /// A format style that displays a floating point number with one decimal place, enforcing the use of a `.` as the decimal separator. static var nonLocalizedDecimal: Self { - nonLocalizedDecimal(decimalPlaces: 1) + nonLocalizedDecimal(places: 1) } /// A format style that displays a floating point number enforcing the use of a `.` as the decimal separator. - /// - Parameter decimalPlaces: The number of decimal places to display. Defaults to 1. - static func nonLocalizedDecimal(decimalPlaces: Int = 1) -> Self { - let precision = max(0, decimalPlaces) + /// - Parameter places: The number of decimal places to display. Defaults to 1. + static func nonLocalizedDecimal(places: Int = 1) -> Self { + let precision = max(0, places) return FloatingPointFormatStyle() .precision(.fractionLength(0...precision)) .locale(Locale(identifier: "en_US")) diff --git a/Sources/Ignite/Modifiers/Opacity.swift b/Sources/Ignite/Modifiers/Opacity.swift index a0fdd941..f291da47 100644 --- a/Sources/Ignite/Modifiers/Opacity.swift +++ b/Sources/Ignite/Modifiers/Opacity.swift @@ -34,9 +34,9 @@ struct OpacityModifier: HTMLModifier { /// - Returns: The modified HTML with opacity applied func body(content: some HTML) -> any HTML { if let percentage, percentage != 100% { - content.style(.opacity, percentage.value.formatted(.nonLocalizedDecimal(decimalPlaces: 3))) + content.style(.opacity, percentage.value.formatted(.nonLocalizedDecimal(places: 3))) } else if let doubleValue, doubleValue != 1 { - content.style(.opacity, doubleValue.formatted(.nonLocalizedDecimal(decimalPlaces: 3))) + content.style(.opacity, doubleValue.formatted(.nonLocalizedDecimal(places: 3))) } content } From ad04c157e44d95a9b7198246865b2e42e2594032 Mon Sep 17 00:00:00 2001 From: Dorian Grolaux <6209874+MrSkwiggs@users.noreply.github.com> Date: Wed, 29 Jan 2025 15:45:07 +0100 Subject: [PATCH 7/7] Fix Swiftlint warnings --- .../Ignite/Extensions/FormatStyle-NonLocalizedDecimal.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/Ignite/Extensions/FormatStyle-NonLocalizedDecimal.swift b/Sources/Ignite/Extensions/FormatStyle-NonLocalizedDecimal.swift index d472968a..b2db0a76 100644 --- a/Sources/Ignite/Extensions/FormatStyle-NonLocalizedDecimal.swift +++ b/Sources/Ignite/Extensions/FormatStyle-NonLocalizedDecimal.swift @@ -8,7 +8,8 @@ import Foundation extension FormatStyle where Self == FloatingPointFormatStyle, FormatInput == Double { - /// A format style that displays a floating point number with one decimal place, enforcing the use of a `.` as the decimal separator. + /// A format style that displays a floating point number with one decimal place, + /// enforcing the use of a `.` as the decimal separator. static var nonLocalizedDecimal: Self { nonLocalizedDecimal(places: 1) } @@ -24,7 +25,8 @@ extension FormatStyle where Self == FloatingPointFormatStyle, FormatInpu } extension FormatStyle where Self == FloatingPointFormatStyle, FormatInput == Float { - /// A format style that displays a floating point number with one decimal place, enforcing the use of a `.` as the decimal separator. + /// A format style that displays a floating point number with one decimal place, + /// enforcing the use of a `.` as the decimal separator. static var nonLocalizedDecimal: Self { nonLocalizedDecimal(places: 1) }