{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}
module Data.CSS.Preprocessor.Text.CounterStyle(CounterStyle(..), CounterSystem(..),
defaultCounter, decimalCounter, simpChineseInformal, cjkDecimal, ethiopic,
isValid, parseCounterStyle, CounterStore'(..), parseCounter, defaultCounterStore,
counterRender, counterRenderMarker, ranges', speakAs', CounterStore) where
import Data.CSS.Syntax.Tokens
import Data.CSS.Syntax.StyleSheet
import Data.FileEmbed (embedStringFile, makeRelativeToProject)
import qualified Data.Text as Txt
import Data.Text (Text, unpack)
import qualified Data.HashMap.Lazy as HM
import Data.HashMap.Lazy (HashMap)
import Data.Maybe (isJust, fromJust)
-- NOTE: No support for image "symbols" yet.
data CounterStyle = CounterStyle {
system :: CounterSystem,
negativePrefix :: Text,
negativeSuffix :: Text,
prefix :: Text,
suffix :: Text,
ranges :: Maybe [(Int, Int)],
padLength :: Int,
padChar :: Text,
fallback :: Maybe CounterStyle,
symbols :: [Text],
additiveSymbols :: [(Int, Text)],
speakAs :: Maybe Text
}
data CounterSystem = Cyclic | Fixed Int | Symbolic | Alphabetic | Numeric
| Additive | Chinese { isSimplified :: Bool } | Ethiopic
defaultCounter, decimalCounter :: CounterStyle
ethiopic, simpChineseInformal, cjkDecimal :: CounterStyle
defaultCounter = CounterStyle {
system = Symbolic,
negativePrefix = "-",
negativeSuffix = "",
prefix = "",
suffix = ". ",
ranges = Nothing,
padLength = 0,
padChar = "",
fallback = Just decimalCounter,
symbols = [], -- Must be overriden!
additiveSymbols = [], -- Alternative requirement!
speakAs = Nothing
}
decimalCounter = defaultCounter {
system = Numeric,
symbols = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"],
fallback = Nothing
}
-- These are here mostly for testing...
cjkDecimal = defaultCounter {
system = Numeric,
ranges = Just [(0, maxBound)],
symbols = ["〇", "一", "二", "三", "四", "五", "六", "七", "八", "九"],
suffix = "、"
}
simpChineseInformal = defaultCounter {
system = Chinese True,
negativePrefix = "负",
symbols = ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九"],
additiveSymbols = [(0, ""), (10, "十"), (100, "百"), (1000, "千")],
suffix = "、",
fallback = Just cjkDecimal
}
ethiopic = defaultCounter {
system = Ethiopic,
symbols = ["", "፩", "፪", "፫", "፬", "፭", "፮", "፯", "፰", "፱"],
additiveSymbols = [(0, ""), (10, "፲"), (20, "፳"), (30, "፴"), (40, "፵"),
(50, "፶"), (60, "፷"), (70, "፸"), (80, "፹"), (90, "፺")],
suffix = "/ "
}
isValid :: CounterStyle -> Bool
isValid CounterStyle { system = Additive, additiveSymbols = [] } = False
isValid self@CounterStyle {
system = Chinese _, symbols = syms, additiveSymbols = markers
} = length syms == 10 && length markers >= 4 && ranges self == Nothing
isValid CounterStyle {
system = Ethiopic, symbols = units, additiveSymbols = tens
} = length units == 10 && length tens == 10
isValid CounterStyle { symbols = [] } = False
isValid _ = True
type CounterStore = HashMap Text CounterStyle
parseCounterProperty :: CounterStore -> (Text, [Token]) ->
CounterStyle -> CounterStyle
parseCounterProperty _ ("system", [Ident "cyclic"]) self = self {system = Cyclic}
parseCounterProperty _ ("system", [Ident "fixed"]) self = self {system = Fixed 1}
parseCounterProperty _ ("system", [Ident "fixed", Number _ (NVInteger x)]) self
= self { system = Fixed $ fromInteger x }
parseCounterProperty _ ("system", [Ident "symbolic"]) self =
self {system = Symbolic }
parseCounterProperty _ ("system", [Ident "alphabetic"]) self =
self { system = Alphabetic }
parseCounterProperty _ ("system", [Ident "numeric"]) self =
self { system = Numeric }
parseCounterProperty _ ("system", [Ident "-argo-chinese", Ident x]) self =
self { system = Chinese (x == "simplified") }
parseCounterProperty _ ("system", [Ident "-argo-ethiopic"]) self =
self { system = Ethiopic }
-- Handled by caller so property overrides work correctly.
parseCounterProperty _ ("system", [Ident "extends", Ident _]) self = self
parseCounterProperty _ ("negative", [x]) self | Just pre <- parseSymbol x =
self { negativePrefix = pre, negativeSuffix = "" }
parseCounterProperty _ ("negative", [x, y]) self
| Just pre <- parseSymbol x, Just suf <- parseSymbol y =
self { negativePrefix = pre, negativeSuffix = suf }
parseCounterProperty _ ("prefix", [x]) self | Just pre <- parseSymbol x =
self { prefix = pre }
parseCounterProperty _ ("suffix", [x]) self | Just suf <- parseSymbol x =
self { suffix = suf }
parseCounterProperty _ ("range", [Ident "auto"]) self = self {ranges = Nothing}
parseCounterProperty _ ("range", toks) self | Just rs <- parseRanges (Comma:toks) =
self { ranges = Just rs }
parseCounterProperty _ ("pad", [Number _ (NVInteger x), y]) self
| Just char <- parseSymbol y = self {
padLength = fromInteger x, padChar = char
}
parseCounterProperty styles ("fallback", [Ident name]) self = self {
fallback = Just $ HM.lookupDefault decimalCounter name styles
}
parseCounterProperty _ ("symbols", toks) self | all (isJust . parseSymbol) toks
= self { symbols = map (fromJust . parseSymbol) toks }
parseCounterProperty _ ("additive-symbols", toks) self
| Just syms <- parseAdditiveSymbols (Comma:toks) =
self { additiveSymbols = syms }
parseCounterProperty _ ("speak-as", [Ident "auto"]) self =
self { speakAs = Nothing }
parseCounterProperty styles ("speak-as", [Ident x]) self
| x `elem` ["bullets", "numbers", "words", "spell-out"] =
self { speakAs = Just x }
| Just super <- HM.lookup x styles = self { speakAs = speakAs super }
| otherwise = self
parseCounterProperty _ _ self = self
parseRanges :: [Token] -> Maybe [(Int, Int)]
parseRanges (Comma:a:b:toks) | Just self <- parseRanges toks = case (a, b) of
(Ident "infinite", Ident "infinite") -> Just ((minBound, maxBound):self)
(Ident "infinite", Number _ (NVInteger x)) ->
Just ((minBound, fromInteger x):self)
(Number _ (NVInteger x), Ident "infinite") ->
Just ((fromInteger x, maxBound):self)
(Number _ (NVInteger x), Number _ (NVInteger y)) ->
Just ((fromInteger x, fromInteger y):self)
_ -> Nothing
parseRanges [] = Just []
parseRanges _ = Nothing
parseAdditiveSymbols :: [Token] -> Maybe [(Int, Text)]
parseAdditiveSymbols (Comma:Number _ (NVInteger x):y:toks)
| Just self <- parseAdditiveSymbols toks, Just sym <- parseSymbol y =
Just ((fromInteger x, sym):self)
parseAdditiveSymbols [] = Just []
parseAdditiveSymbols _ = Nothing
parseCounterStyle :: CounterStore -> [Token] -> (CounterStore, [Token])
parseCounterStyle store (Whitespace:toks) = parseCounterStyle store toks
parseCounterStyle store (Ident name:toks)
| ((props, ""), toks') <- parseProperties toks =
let super = case Prelude.lookup "system" props of
Just [Ident "extends", Ident name'] ->
HM.lookupDefault decimalCounter name' store
_ -> defaultCounter
style = foldr (parseCounterProperty store) super props
in (HM.insert name style store, toks')
parseCounterStyle store toks = (store, skipAtRule toks)
parseSymbol :: Token -> Maybe Text
parseSymbol (Ident x) = Just x
parseSymbol (String x) = Just x
parseSymbol _ = Nothing
data CounterStore' = CounterStore { unwrap :: CounterStore }
instance StyleSheet CounterStore' where
addRule self _ = self
addAtRule (CounterStore self) "counter-style" toks =
let (self', toks') = parseCounterStyle self toks
in (CounterStore self', toks')
addAtRule self _ toks = (self, skipAtRule toks)
defaultCounterStore :: CounterStore'
defaultCounterStore =
parse (CounterStore HM.empty) $ Txt.pack
$(makeRelativeToProject "src/Data/CSS/Preprocessor/Text/counter-styles.css" >>=
embedStringFile)
---
fallbackSym :: Text
fallbackSym = "\0"
counterRenderCore :: CounterStyle -> Int -> Text
counterRenderCore CounterStyle { system = Fixed n, symbols = syms } x
| x - n < length syms && x >= n = syms !! (x - n)
| otherwise = fallbackSym
counterRenderCore _ x | x < 0 = fallbackSym
counterRenderCore CounterStyle { system = Cyclic, symbols = syms } x =
syms !! (pred x `rem` length syms)
counterRenderCore CounterStyle { system = Symbolic, symbols = syms } x =
succ (quot x' n) `Txt.replicate` (syms !! rem x' n)
where (n, x') = (length syms, pred x)
counterRenderCore CounterStyle { system = Alphabetic, symbols = syms } x = inner x
where
n = length syms
inner 0 = ""
inner y = let x' = pred y in inner (quot x' n) `Txt.append` (syms !! rem x' n)
counterRenderCore CounterStyle { system = Numeric, symbols = syms } 0 = syms !! 0
counterRenderCore CounterStyle { system = Numeric, symbols = syms } x = inner x
where
n = length syms
inner 0 = ""
inner y = inner (quot y n) `Txt.append` (syms !! rem y n)
counterRenderCore CounterStyle { system = Additive, additiveSymbols = syms } 0
| Just sym <- Prelude.lookup 0 syms = sym
| otherwise = fallbackSym
counterRenderCore CounterStyle { system = Additive, additiveSymbols = syms } w
| '\0' `elem'` inner syms w = fallbackSym
| otherwise = inner syms w
where
elem' ch txt = elem ch $ unpack txt
inner _ 0 = ""
inner ((0, _):syms') x = inner syms' x
inner ((weight, _):syms') x | weight > x = inner syms' x
inner ((weight, sym):syms') x =
Txt.replicate reps sym `Txt.append` inner syms' x'
where
reps = quot x weight
x' = x - weight * reps
inner [] _ = "\0" -- Run fallback counter!
-- Following https://w3c.github.io/csswg-drafts/css-counter-styles-3/#limited-chinese
-- 1. If the counter value is 0, the representation is the character for 0
-- specified for the given counter style. Skip the rest of this algorithm.
counterRenderCore CounterStyle { system = Chinese _, symbols = (sym:_) } 0 = sym
counterRenderCore CounterStyle {
system = Chinese simplified, symbols = syms, additiveSymbols = markers
} x = Txt.concat $ map renderDigit $ collapse0s $
reverse $ enumerate $ decimalDigits x
where
-- Implements step 4.
collapse0s ((i, 0):digits) = inner digits
where
inner ((_, 0):digits') = inner digits'
-- Drop trailing 0s
inner [] = []
-- Collapse any remaining (consecutive?) zeroes into a single 0 digit.
inner digits' = (i, 0):collapse0s digits'
collapse0s (digit:digits) = digit:collapse0s digits
collapse0s [] = []
renderDigit (_, 0) = syms !! 0 -- Don't append digit marker for zero, step 2.
-- 3. For the informal styles, if the counter value is between 10 and 19,
-- remove the 10s digit (leave the digit marker).
renderDigit (1,1) | simplified, [_, _] <- decimalDigits x = markers' !! 1
-- Select characters per steps 2 & 5
renderDigit (place, digit) = Txt.concat [syms !! digit, markers' !! place]
markers' = map snd markers
-- Following https://w3c.github.io/csswg-drafts/css-counter-styles-3/#ethiopic-numeric-counter-style
-- 1. If the number is 1, return "፩" (U+1369).
counterRenderCore CounterStyle { system = Ethiopic, symbols = (_:sym:_) } 1 = sym
counterRenderCore CounterStyle {
system = Ethiopic, symbols = unitSyms, additiveSymbols = tenSyms
} x = Txt.concat $ renderPairs True $
reverse $ enumerate $ pairDigits $ decimalDigits x
where
-- 2. Split the number into groups of two digits,
-- starting with the least significant decimal digit.
pairDigits (units:tens:digits) = (tens,units):pairDigits digits
pairDigits [units] = (0, units):[]
pairDigits [] = []
renderPairs isBigEnd (group:groups) =
renderPair isBigEnd group:renderPairs False groups
renderPairs _ [] = []
-- Handle step 4's exceptions.
renderPair' :: Bool -> (Int, (Int, Int)) -> Text
renderPair' _ (_,(0, 0)) = ""
renderPair' True (_, (0,1)) = ""
renderPair' _ (i, (0,1)) | odd i = ""
-- Step 5
renderPair' _ (_, (tens, units)) =
(map snd tenSyms !! tens) `Txt.append` (unitSyms !! units)
-- Step 6 & 7
renderPair _ (i, (0,0)) | odd i = ""
renderPair isBigEnd (0, group) = renderPair' isBigEnd (0, group)
renderPair isBigEnd (i, group)
| odd i = renderPair' isBigEnd (i, group) `Txt.append` "፻"
| even i = renderPair' isBigEnd (i, group) `Txt.append` "፼"
renderPair _ _ = "" -- Silence warnings, above case should not fallthrough.
decimalDigits :: Int -> [Int]
decimalDigits 0 = []
decimalDigits x = rem x 10:decimalDigits (quot x 10)
enumerate :: [a] -> [(Int, a)]
enumerate = zip $ enumFrom 0
counterRenderMarker :: CounterStyle -> Int -> Text
counterRenderMarker self x =
Txt.concat [prefix self, counterRender self x, suffix self]
counterRender :: CounterStyle -> Int -> Text
counterRender self@CounterStyle { fallback = Just self' } x
| not $ inRange x $ ranges' self = counterRender self' x
| counterRenderCore self x == fallbackSym = counterRender self' x
where
inRange y ((start, end):rest)
| y >= start && y <= end = True
| otherwise = inRange y rest
inRange _ [] = False
counterRender self@CounterStyle { padLength = m, padChar = pad } x
| Fixed _ <- system self = text -- Handles negatives specially here.
| x < 0 = Txt.concat [
negativePrefix self,
counterRender self { ranges = Just [(0, maxBound)] } $ -x, -- No fallback!
negativeSuffix self
]
| text == fallbackSym = Txt.pack $ show x -- NOTE: Shouldn't happen
| n < m = Txt.replicate (m - n) pad `Txt.append` text
| otherwise = text
where
text = counterRenderCore self x
n = Txt.length text
infiniteRange :: [(Int, Int)]
infiniteRange = [(minBound, maxBound)]
ranges' :: CounterStyle -> [(Int, Int)]
ranges' CounterStyle { ranges = Just ret } = ret
ranges' CounterStyle { system = Cyclic } = infiniteRange
ranges' CounterStyle { system = Numeric } = infiniteRange
ranges' CounterStyle { system = Fixed _ } = infiniteRange
ranges' CounterStyle { system = Alphabetic } = [(1, maxBound)]
ranges' CounterStyle { system = Symbolic } = [(1, maxBound)]
ranges' CounterStyle { system = Additive } = [(0, maxBound)]
ranges' CounterStyle { system = Chinese _ } = [(-9999, 9999)]
ranges' CounterStyle { system = Ethiopic } = [(1, maxBound)]
speakAs' :: CounterStyle -> Text
speakAs' CounterStyle { speakAs = Just ret } = ret
speakAs' CounterStyle { system = Alphabetic } = "spell-out"
speakAs' CounterStyle { system = Cyclic } = "bullets"
speakAs' _ = "numbers"
---
parseCounter :: CounterStore -> [Token] -> Maybe (CounterStyle, [Token])
parseCounter _ (Function "symbols":Ident name:toks)
| Just system' <- Prelude.lookup name [
("cyclic", Cyclic), ("numeric", Numeric), ("alphabetic", Alphabetic),
("symbolic", Symbolic), ("fixed", Fixed 1)
], Just (syms, toks') <- parseArgs toks =
Just (defaultCounter { system = system', symbols = syms }, toks')
where
parseArgs (String sym:toks') | Just (syms,tail') <- parseArgs toks' =
Just (sym:syms, tail')
parseArgs (RightParen:toks') = Just ([],toks')
parseArgs _ = Nothing
parseCounter store (Ident name:toks)
| Just ret <- HM.lookup name store, isValid ret = Just (ret, toks)
| otherwise = Just (decimalCounter, toks)
parseCounter _ (String sym:toks) =
Just (defaultCounter {system = Cyclic, symbols = [sym], suffix = " "}, toks)
parseCounter _ _ = Nothing