| /* |
| * Copyright (C) 2022-2024 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "CSSCounterStyle.h" |
| |
| #include "CSSCounterStyleDescriptors.h" |
| #include "CSSCounterStyleRegistry.h" |
| #include <cmath> |
| #include <wtf/Assertions.h> |
| #include <wtf/text/MakeString.h> |
| #include <wtf/text/StringBuilder.h> |
| #include <wtf/text/TextBreakIterator.h> |
| #include <wtf/unicode/CharacterNames.h> |
| |
| namespace WebCore { |
| |
| // https://www.w3.org/TR/css-counter-styles-3/#cyclic-system |
| String CSSCounterStyle::counterForSystemCyclic(int value) const |
| { |
| auto amountOfSymbols = symbols().size(); |
| ASSERT(amountOfSymbols > 0); |
| |
| // For avoiding subtracting -1 from INT_MAX we will sum-up amountOfSymbols in case the value is not positive. |
| // This works because x % y = (x + y) % y |
| unsigned symbolIndex = static_cast<unsigned>(value > 0 ? value : value + amountOfSymbols); |
| symbolIndex = (symbolIndex - 1) % amountOfSymbols; |
| ASSERT(static_cast<unsigned>(symbolIndex) < amountOfSymbols); |
| |
| return symbols().at(static_cast<unsigned>(symbolIndex)).text; |
| } |
| |
| // https://www.w3.org/TR/css-counter-styles-3/#fixed-system |
| String CSSCounterStyle::counterForSystemFixed(int value) const |
| { |
| if (value < firstSymbolValueForFixedSystem()) |
| return { }; |
| unsigned valueOffset = value - firstSymbolValueForFixedSystem(); |
| if (valueOffset >= symbols().size()) |
| return { }; |
| return symbols().at(valueOffset).text; |
| } |
| |
| // https://www.w3.org/TR/css-counter-styles-3/#symbolic-system |
| String CSSCounterStyle::counterForSystemSymbolic(unsigned value) const |
| { |
| auto amountOfSymbols = symbols().size(); |
| ASSERT(amountOfSymbols > 0); |
| |
| if (value < 1) |
| return { }; |
| |
| unsigned symbolIndex = ((value - 1) % amountOfSymbols); |
| unsigned frequency = static_cast<unsigned>(std::ceil(static_cast<float>(value) / amountOfSymbols)); |
| |
| StringBuilder result; |
| for (unsigned i = 0; i < frequency; ++i) |
| result.append(symbols().at(symbolIndex).text); |
| return result.toString(); |
| } |
| |
| // https://www.w3.org/TR/css-counter-styles-3/#alphabetic-system |
| String CSSCounterStyle::counterForSystemAlphabetic(unsigned value) const |
| { |
| auto amountOfSymbols = symbols().size(); |
| ASSERT(amountOfSymbols >= 2); |
| |
| if (value < 1) |
| return { }; |
| |
| Vector<String> reversed; |
| while (value) { |
| value -= 1; |
| reversed.append(symbols().at(value % amountOfSymbols).text); |
| value = std::floor(value / amountOfSymbols); |
| } |
| StringBuilder result; |
| for (auto iter = reversed.rbegin(); iter != reversed.rend(); ++iter) |
| result.append(*iter); |
| return result.toString(); |
| } |
| |
| // https://www.w3.org/TR/css-counter-styles-3/#numeric-system |
| String CSSCounterStyle::counterForSystemNumeric(unsigned value) const |
| { |
| auto amountOfSymbols = symbols().size(); |
| ASSERT(amountOfSymbols >= 2); |
| |
| if (!value) |
| return symbols().at(0).text; |
| |
| Vector<String> reversed; |
| while (value) { |
| reversed.append(symbols().at(value % amountOfSymbols).text); |
| value = static_cast<unsigned>(std::floor(value / amountOfSymbols)); |
| } |
| StringBuilder result; |
| for (auto iter = reversed.rbegin(); iter != reversed.rend(); ++iter) |
| result.append(*iter); |
| return result.toString(); |
| } |
| |
| // https://www.w3.org/TR/css-counter-styles-3/#additive-system |
| String CSSCounterStyle::counterForSystemAdditive(unsigned value) const |
| { |
| auto& additiveSymbols = this->additiveSymbols(); |
| if (!value) { |
| for (auto& [symbol, weight] : additiveSymbols) { |
| if (!weight) |
| return symbol.text; |
| } |
| return { }; |
| } |
| |
| StringBuilder result; |
| auto appendToResult = [&](const String& symbol, unsigned frequency) { |
| for (unsigned i = 0; i < frequency; ++i) |
| result.append(symbol); |
| }; |
| |
| for (auto& [symbol, weight] : additiveSymbols) { |
| if (!weight || weight > value) |
| continue; |
| auto repetitions = static_cast<unsigned>(std::floor(value / weight)); |
| appendToResult(symbol.text, repetitions); |
| value -= weight * repetitions; |
| if (!value) |
| return result.toString(); |
| } |
| return { }; |
| } |
| |
| enum class Formality : bool { Informal, Formal }; |
| |
| // This table format was derived from an old draft of the CSS specification: 3 group markers, 3 digit markers, 10 digits, negative sign. |
| static String counterForSystemCJK(int number, const std::array<char16_t, 17>& table, Formality formality) |
| { |
| enum AbstractCJKCharacter { |
| NoChar, |
| SecondGroupMarker, ThirdGroupMarker, FourthGroupMarker, |
| SecondDigitMarker, ThirdDigitMarker, FourthDigitMarker, |
| Digit0, Digit1, Digit2, Digit3, Digit4, |
| Digit5, Digit6, Digit7, Digit8, Digit9, |
| NegativeSign |
| }; |
| |
| if (!number) |
| return span(table[Digit0 - 1]); |
| |
| ASSERT(number != std::numeric_limits<int>::min()); |
| bool needsNegativeSign = number < 0; |
| if (needsNegativeSign) |
| number = -number; |
| |
| constexpr unsigned groupLength = 8; // 4 digits, 3 digit markers, and a group marker |
| constexpr unsigned bufferLength = 4 * groupLength; |
| std::array<AbstractCJKCharacter, bufferLength> buffer; |
| buffer.fill(NoChar); |
| |
| for (int i = 0; i < 4; ++i) { |
| int groupValue = number % 10000; |
| number /= 10000; |
| |
| // Process least-significant group first, but put it in the buffer last. |
| auto group = std::span { buffer }.subspan((3 - i) * groupLength); |
| |
| if (groupValue && i) |
| group[7] = static_cast<AbstractCJKCharacter>(SecondGroupMarker - 1 + i); |
| |
| // Put in the four digits and digit markers for any non-zero digits. |
| group[6] = static_cast<AbstractCJKCharacter>(Digit0 + (groupValue % 10)); |
| if (number || groupValue > 9) { |
| int digitValue = ((groupValue / 10) % 10); |
| group[4] = static_cast<AbstractCJKCharacter>(Digit0 + digitValue); |
| if (digitValue) |
| group[5] = SecondDigitMarker; |
| } |
| if (number || groupValue > 99) { |
| int digitValue = ((groupValue / 100) % 10); |
| group[2] = static_cast<AbstractCJKCharacter>(Digit0 + digitValue); |
| if (digitValue) |
| group[3] = ThirdDigitMarker; |
| } |
| if (number || groupValue > 999) { |
| int digitValue = groupValue / 1000; |
| group[0] = static_cast<AbstractCJKCharacter>(Digit0 + digitValue); |
| if (digitValue) |
| group[1] = FourthDigitMarker; |
| } |
| |
| if (formality == Formality::Informal && groupValue < 20) { |
| // Remove the tens digit, but leave the marker. |
| ASSERT(group[4] == NoChar || group[4] == Digit0 || group[4] == Digit1); |
| group[4] = NoChar; |
| } |
| |
| if (!number) |
| break; |
| } |
| |
| // Convert into characters, omitting consecutive runs of digit0 and trailing digit0. |
| unsigned length = 0; |
| std::array<char16_t, bufferLength + 1> characters; |
| auto last = NoChar; |
| if (needsNegativeSign) |
| characters[length++] = table[NegativeSign - 1]; |
| for (unsigned i = 0; i < bufferLength; ++i) { |
| auto character = buffer[i]; |
| if (character != NoChar) { |
| if (character != Digit0 || last != Digit0) |
| characters[length++] = table[character - 1]; |
| last = character; |
| } |
| } |
| if (last == Digit0) |
| --length; |
| |
| return std::span<const char16_t> { characters }.first(length); |
| } |
| |
| String CSSCounterStyle::counterForSystemDisclosureClosed(WritingMode writingMode) |
| { |
| if (writingMode.isVerticalTypographic()) |
| return span(writingMode.isInlineTopToBottom() ? blackDownPointingTriangle : blackUpPointingTriangle); |
| return span(writingMode.isBidiLTR() ? blackRightPointingTriangle : blackLeftPointingTriangle); |
| } |
| |
| String CSSCounterStyle::counterForSystemDisclosureOpen(WritingMode writingMode) |
| { |
| switch (writingMode.blockDirection()) { |
| case FlowDirection::TopToBottom: |
| return span(blackDownPointingTriangle); |
| case FlowDirection::BottomToTop: |
| return span(blackUpPointingTriangle); |
| case FlowDirection::LeftToRight: |
| return span(blackRightPointingTriangle); |
| case FlowDirection::RightToLeft: |
| return span(blackLeftPointingTriangle); |
| } |
| ASSERT_NOT_REACHED(); |
| return { }; |
| } |
| |
| String CSSCounterStyle::counterForSystemSimplifiedChineseInformal(int value) |
| { |
| static constexpr std::array<char16_t, 17> simplifiedChineseInformalTable { |
| 0x842C, 0x5104, 0x5146, // These three group markers are probably wrong; OK because we don't use this on big enough numbers. |
| 0x5341, 0x767E, 0x5343, |
| 0x96F6, 0x4E00, 0x4E8C, 0x4E09, 0x56DB, |
| 0x4E94, 0x516D, 0x4E03, 0x516B, 0x4E5D, |
| 0x8D1F |
| }; |
| return counterForSystemCJK(value, simplifiedChineseInformalTable, Formality::Informal); |
| } |
| |
| String CSSCounterStyle::counterForSystemSimplifiedChineseFormal(int value) |
| { |
| static constexpr std::array<char16_t, 17> simplifiedChineseFormalTable { |
| 0x842C, 0x5104, 0x5146, // These three group markers are probably wrong; OK because we don't use this on big enough numbers. |
| 0x62FE, 0x4F70, 0x4EDF, |
| 0x96F6, 0x58F9, 0x8D30, 0x53C1, 0x8086, |
| 0x4F0D, 0x9646, 0x67D2, 0x634C, 0x7396, |
| 0x8D1F |
| }; |
| return counterForSystemCJK(value, simplifiedChineseFormalTable, Formality::Formal); |
| } |
| |
| String CSSCounterStyle::counterForSystemTraditionalChineseInformal(int value) |
| { |
| static constexpr std::array<char16_t, 17> traditionalChineseInformalTable { |
| 0x842C, 0x5104, 0x5146, |
| 0x5341, 0x767E, 0x5343, |
| 0x96F6, 0x4E00, 0x4E8C, 0x4E09, 0x56DB, |
| 0x4E94, 0x516D, 0x4E03, 0x516B, 0x4E5D, |
| 0x8CA0 |
| }; |
| return counterForSystemCJK(value, traditionalChineseInformalTable, Formality::Informal); |
| } |
| |
| String CSSCounterStyle::counterForSystemTraditionalChineseFormal(int value) |
| { |
| static constexpr std::array<char16_t, 17> traditionalChineseFormalTable { |
| 0x842C, 0x5104, 0x5146, // These three group markers are probably wrong; OK because we don't use this on big enough numbers. |
| 0x62FE, 0x4F70, 0x4EDF, |
| 0x96F6, 0x58F9, 0x8CB3, 0x53C3, 0x8086, |
| 0x4F0D, 0x9678, 0x67D2, 0x634C, 0x7396, |
| 0x8CA0 |
| }; |
| return counterForSystemCJK(value, traditionalChineseFormalTable, Formality::Formal); |
| } |
| |
| String CSSCounterStyle::counterForSystemEthiopicNumeric(unsigned value) |
| { |
| ASSERT(value >= 1); |
| |
| if (value == 1) { |
| char16_t ethiopicDigitOne = 0x1369; |
| return span(ethiopicDigitOne); |
| } |
| |
| // Split the number into groups of two digits, starting with the least significant decimal digit. |
| std::array<uint8_t, 5> groups; |
| for (auto& group : groups) { |
| group = value % 100; |
| value /= 100; |
| } |
| |
| std::array<char16_t, groups.size() * 3> buffer; |
| unsigned length = 0; |
| bool isMostSignificantGroup = true; |
| for (int i = groups.size() - 1; i >= 0; --i) { |
| auto value = groups[i]; |
| bool isOddIndex = i & 1; |
| // If the group has the value zero, or if the group is the most significant one and has the value 1, |
| // or if the group has an odd index (as given in the previous step) and has the value 1, |
| // then remove the digits (but leave the group, so it still has a separator appended below). |
| if (!(value == 1 && (isMostSignificantGroup || isOddIndex))) { |
| if (auto tens = value / 10) |
| buffer[length++] = 0x1371 + tens; |
| if (auto ones = value % 10) |
| buffer[length++] = 0x1368 + ones; |
| } |
| if (value && isOddIndex) |
| buffer[length++] = 0x137B; |
| if ((value || !isMostSignificantGroup) && !isOddIndex && i) |
| buffer[length++] = 0x137C; |
| if (value) |
| isMostSignificantGroup = false; |
| } |
| |
| return std::span<const char16_t> { buffer }.first(length); |
| } |
| |
| String CSSCounterStyle::initialRepresentation(int value, WritingMode writingMode) const |
| { |
| unsigned absoluteValue = std::abs(value); |
| switch (system()) { |
| case CSSCounterStyleDescriptors::System::Cyclic: |
| return counterForSystemCyclic(value); |
| case CSSCounterStyleDescriptors::System::Numeric: |
| return counterForSystemNumeric(absoluteValue); |
| case CSSCounterStyleDescriptors::System::Alphabetic: |
| return counterForSystemAlphabetic(absoluteValue); |
| case CSSCounterStyleDescriptors::System::Symbolic: |
| return counterForSystemSymbolic(absoluteValue); |
| case CSSCounterStyleDescriptors::System::Additive: |
| return counterForSystemAdditive(absoluteValue); |
| case CSSCounterStyleDescriptors::System::Fixed: |
| return counterForSystemFixed(value); |
| case CSSCounterStyleDescriptors::System::DisclosureClosed: |
| return counterForSystemDisclosureClosed(writingMode); |
| case CSSCounterStyleDescriptors::System::DisclosureOpen: |
| return counterForSystemDisclosureOpen(writingMode); |
| case CSSCounterStyleDescriptors::System::SimplifiedChineseInformal: |
| return counterForSystemSimplifiedChineseInformal(value); |
| case CSSCounterStyleDescriptors::System::SimplifiedChineseFormal: |
| return counterForSystemSimplifiedChineseFormal(value); |
| case CSSCounterStyleDescriptors::System::TraditionalChineseInformal: |
| return counterForSystemTraditionalChineseInformal(value); |
| case CSSCounterStyleDescriptors::System::TraditionalChineseFormal: |
| return counterForSystemTraditionalChineseFormal(value); |
| case CSSCounterStyleDescriptors::System::EthiopicNumeric: |
| return counterForSystemEthiopicNumeric(value); |
| case CSSCounterStyleDescriptors::System::Extends: |
| // CounterStyle with extends system should have been promoted to another system at this point |
| ASSERT_NOT_REACHED(); |
| break; |
| } |
| return { }; |
| } |
| |
| String CSSCounterStyle::fallbackText(int value, WritingMode writingMode) |
| { |
| if (m_isFallingBack || !fallback().get()) { |
| m_isFallingBack = false; |
| return CSSCounterStyleRegistry::decimalCounter()->text(value, writingMode); |
| } |
| m_isFallingBack = true; |
| auto fallbackText = fallback()->text(value, writingMode); |
| m_isFallingBack = false; |
| return fallbackText; |
| } |
| |
| String CSSCounterStyle::text(int value, WritingMode writingMode) |
| { |
| if (!isInRange(value)) |
| return fallbackText(value, writingMode); |
| |
| auto result = initialRepresentation(value, writingMode); |
| if (result.isNull()) |
| return fallbackText(value, writingMode); |
| applyPadSymbols(result, value); |
| if (shouldApplyNegativeSymbols(value)) |
| applyNegativeSymbols(result); |
| |
| return result; |
| } |
| |
| bool CSSCounterStyle::shouldApplyNegativeSymbols(int value) const |
| { |
| auto system = this->system(); |
| return value < 0 && (system == CSSCounterStyleDescriptors::System::Symbolic || system == CSSCounterStyleDescriptors::System::Numeric || system == CSSCounterStyleDescriptors::System::Alphabetic || system == CSSCounterStyleDescriptors::System::Additive); |
| } |
| |
| void CSSCounterStyle::applyNegativeSymbols(String& text) const |
| { |
| text = negative().m_suffix.text.isEmpty() ? makeString(negative().m_prefix.text, text) : makeString(negative().m_prefix.text, text, negative().m_suffix.text); |
| } |
| |
| void CSSCounterStyle::applyPadSymbols(String& text, int value) const |
| { |
| // FIXME: should we cap pad minimum length? |
| if (pad().m_padMinimumLength <= 0) |
| return; |
| |
| int numberOfSymbolsToAdd = static_cast<int>(pad().m_padMinimumLength - WTF::numGraphemeClusters(text)); |
| if (shouldApplyNegativeSymbols(value)) |
| numberOfSymbolsToAdd -= static_cast<int>(WTF::numGraphemeClusters(negative().m_prefix.text) + WTF::numGraphemeClusters(negative().m_suffix.text)); |
| |
| String padText; |
| for (int i = 0; i < numberOfSymbolsToAdd; ++i) |
| padText = makeString(padText, pad().m_padSymbol.text); |
| text = makeString(padText, text); |
| } |
| |
| bool CSSCounterStyle::isInRange(int value) const |
| { |
| if (isAutoRange()) { |
| switch (system()) { |
| case CSSCounterStyleDescriptors::System::Cyclic: |
| case CSSCounterStyleDescriptors::System::Numeric: |
| case CSSCounterStyleDescriptors::System::Fixed: |
| case CSSCounterStyleDescriptors::System::DisclosureClosed: |
| case CSSCounterStyleDescriptors::System::DisclosureOpen: |
| return true; |
| case CSSCounterStyleDescriptors::System::Alphabetic: |
| case CSSCounterStyleDescriptors::System::Symbolic: |
| case CSSCounterStyleDescriptors::System::EthiopicNumeric: |
| return value >= 1; |
| case CSSCounterStyleDescriptors::System::Additive: |
| return value >= 0; |
| case CSSCounterStyleDescriptors::System::SimplifiedChineseInformal: |
| case CSSCounterStyleDescriptors::System::SimplifiedChineseFormal: |
| case CSSCounterStyleDescriptors::System::TraditionalChineseInformal: |
| case CSSCounterStyleDescriptors::System::TraditionalChineseFormal: |
| return value >= -9999 && value <= 9999; |
| case CSSCounterStyleDescriptors::System::Extends: |
| ASSERT_NOT_REACHED(); |
| return true; |
| } |
| } |
| |
| for (const auto& [lowerBound, higherBound] : ranges()) { |
| if (value >= lowerBound && value <= higherBound) |
| return true; |
| } |
| return false; |
| } |
| |
| CSSCounterStyle::CSSCounterStyle(const CSSCounterStyleDescriptors& descriptors, bool isPredefinedCounterStyle) |
| : m_descriptors { descriptors } |
| , m_predefinedCounterStyle { isPredefinedCounterStyle } |
| { |
| } |
| |
| Ref<CSSCounterStyle> CSSCounterStyle::create(const CSSCounterStyleDescriptors& descriptors, bool isPredefinedCounterStyle) |
| { |
| return adoptRef(*new CSSCounterStyle(descriptors, isPredefinedCounterStyle)); |
| } |
| |
| void CSSCounterStyle::setFallbackReference(Ref<CSSCounterStyle>&& fallback) |
| { |
| m_fallbackReference = WeakPtr { fallback }; |
| } |
| |
| // The counter's system value is promoted to the value of the counter we are extending. |
| void CSSCounterStyle::extendAndResolve(const CSSCounterStyle& extendedCounterStyle) |
| { |
| m_descriptors.m_isExtendedResolved = true; |
| |
| setSystem(extendedCounterStyle.system()); |
| setFirstSymbolValueForFixedSystem(extendedCounterStyle.firstSymbolValueForFixedSystem()); |
| |
| if (!explicitlySetDescriptors().contains(CSSCounterStyleDescriptors::ExplicitlySetDescriptors::Negative)) |
| setNegative(extendedCounterStyle.negative()); |
| if (!explicitlySetDescriptors().contains(CSSCounterStyleDescriptors::ExplicitlySetDescriptors::Prefix)) |
| setPrefix(extendedCounterStyle.prefix()); |
| if (!explicitlySetDescriptors().contains(CSSCounterStyleDescriptors::ExplicitlySetDescriptors::Suffix)) |
| setSuffix(extendedCounterStyle.suffix()); |
| if (!explicitlySetDescriptors().contains(CSSCounterStyleDescriptors::ExplicitlySetDescriptors::Range)) |
| setRanges(extendedCounterStyle.ranges()); |
| if (!explicitlySetDescriptors().contains(CSSCounterStyleDescriptors::ExplicitlySetDescriptors::Pad)) |
| setPad(extendedCounterStyle.pad()); |
| if (!explicitlySetDescriptors().contains(CSSCounterStyleDescriptors::ExplicitlySetDescriptors::Fallback)) { |
| setFallbackName(extendedCounterStyle.fallbackName()); |
| m_fallbackReference = extendedCounterStyle.m_fallbackReference; |
| } |
| if (!explicitlySetDescriptors().contains(CSSCounterStyleDescriptors::ExplicitlySetDescriptors::Symbols)) |
| setSymbols(extendedCounterStyle.symbols()); |
| if (!explicitlySetDescriptors().contains(CSSCounterStyleDescriptors::ExplicitlySetDescriptors::AdditiveSymbols)) |
| setAdditiveSymbols(extendedCounterStyle.additiveSymbols()); |
| if (!explicitlySetDescriptors().contains(CSSCounterStyleDescriptors::ExplicitlySetDescriptors::SpeakAs)) |
| setSpeakAs(extendedCounterStyle.speakAs()); |
| } |
| } // namespace WebCore |