packages feed

lipstick-0.1.0.0: scripts/GenUnicodeTable.hs

-- | Generate internal/Lipstick/Internal/UnicodeTable.hs from the Unicode
-- Character Database.
--
-- Usage:
--
-- > runghc scripts/GenUnicodeTable.hs VERSION UCD_DIR > internal/Lipstick/Internal/UnicodeTable.hs
--
-- where UCD_DIR holds, side by side, EastAsianWidth.txt,
-- GraphemeBreakProperty.txt, emoji-data.txt and DerivedCoreProperties.txt,
-- downloaded from https://www.unicode.org/Public/VERSION/ucd/ (the second
-- from auxiliary/, the third from emoji/).
--
-- The tests pin the tables to Unicode 16.0.0. To move to another version,
-- also update the version that test/Main.hs and the Lipstick.Width
-- doctest expect, and test/data/GraphemeBreakTest.txt (from auxiliary/).
--
-- The table maps every code point to a packed Word16:
--
-- * bits 0–3: grapheme cluster break property (see gcbCode)
-- * bit 4: East Asian Width is W or F
-- * bit 5: Extended_Pictographic
-- * bit 6: Emoji
-- * bits 7–8: Indic_Conjunct_Break (0 None, 1 Linker, 2 Consonant, 3 Extend)
--
-- and is stored run-length encoded as the start of each run and its value.
module Main (main) where

import Control.Monad (forM_, when)
import Data.Array.ST (newArray, readArray, runSTUArray, writeArray)
import Data.Array.Unboxed (UArray, assocs)
import Data.Bits (complement, shiftL, (.&.), (.|.))
import Data.Char (isSpace)
import Data.List (intercalate, isPrefixOf)
import Data.Word (Word16)
import Numeric (readHex)
import System.Environment (getArgs)
import System.FilePath ((</>))

main :: IO ()
main = do
  [version, dir] <- getArgs
  eaw <- readFile (dir </> "EastAsianWidth.txt")
  gcb <- readFile (dir </> "GraphemeBreakProperty.txt")
  emoji <- readFile (dir </> "emoji-data.txt")
  dcp <- readFile (dir </> "DerivedCoreProperties.txt")

  let wide =
        [ r
        | (r, [w]) <- missing eaw ++ entries eaw
        , w `elem` ["W", "F"]
        ]
      -- @missing lines give defaults and come first, so later explicit
      -- entries override them; record which ranges are explicitly not wide.
      notWide = [r | (r, [w]) <- entries eaw, w `notElem` ["W", "F"]]
      gcbs = [(r, gcbCode p) | (r, [p]) <- entries gcb]
      pict = [r | (r, ["Extended_Pictographic"]) <- entries emoji]
      emo = [r | (r, ["Emoji"]) <- entries emoji]
      incb = [(r, incbCode v) | (r, ["InCB", v]) <- entries dcp]

      table :: UArray Int Word16
      table = runSTUArray $ do
        arr <- newArray (0, 0x10FFFF) 0
        let paint (lo, hi) f = forM_ [lo .. hi] $ \i -> readArray arr i >>= writeArray arr i . f
        forM_ wide $ \r -> paint r (.|. bit 4)
        forM_ notWide $ \r -> paint r (.&. complement (bit 4))
        forM_ gcbs $ \(r, v) -> paint r (\x -> (x .&. complement 0xF) .|. v)
        forM_ pict $ \r -> paint r (.|. bit 5)
        forM_ emo $ \r -> paint r (.|. bit 6)
        forM_ incb $ \(r, v) -> paint r (\x -> (x .&. complement (3 `shiftL` 7)) .|. (v `shiftL` 7))
        pure arr

      runs = compress (assocs table)

  when (null runs) (error "empty table")
  putStr (render version runs)

bit :: Int -> Word16
bit n = 1 `shiftL` n

gcbCode :: String -> Word16
gcbCode p = case p of
  "CR" -> 1
  "LF" -> 2
  "Control" -> 3
  "Extend" -> 4
  "ZWJ" -> 5
  "Regional_Indicator" -> 6
  "Prepend" -> 7
  "SpacingMark" -> 8
  "L" -> 9
  "V" -> 10
  "T" -> 11
  "LV" -> 12
  "LVT" -> 13
  _ -> error ("unknown grapheme break property " ++ p)

incbCode :: String -> Word16
incbCode v = case v of
  "Linker" -> 1
  "Consonant" -> 2
  "Extend" -> 3
  _ -> error ("unknown InCB value " ++ v)

-- | Data lines: a code point range and the semicolon-separated fields.
entries :: String -> [((Int, Int), [String])]
entries = concatMap entry . lines
  where
    entry l = case splitFields (takeWhile (/= '#') l) of
      r : fields | not (null r) -> [(range r, fields)]
      _ -> []

-- | Default values from "# @missing: 3400..4DBF; W" lines.
missing :: String -> [((Int, Int), [String])]
missing = concatMap entry . lines
  where
    prefix = "# @missing: "
    entry l
      | prefix `isPrefixOf` l = case splitFields (drop (length prefix) l) of
          r : fields -> [(range r, fields)]
          [] -> []
      | otherwise = []

splitFields :: String -> [String]
splitFields s = case map trim (splitOn ';' s) of
  [""] -> []
  fs -> fs
  where
    splitOn c xs = case break (== c) xs of
      (a, []) -> [a]
      (a, _ : rest) -> a : splitOn c rest
    trim = reverse . dropWhile isSpace . reverse . dropWhile isSpace

range :: String -> (Int, Int)
range r = case break (== '.') r of
  (lo, []) -> (hexInt lo, hexInt lo)
  (lo, _ : _ : hi) -> (hexInt lo, hexInt hi)
  _ -> error ("bad range " ++ r)
  where
    hexInt h = case readHex h of
      [(n, "")] -> n
      _ -> error ("bad code point " ++ h)

compress :: [(Int, Word16)] -> [(Int, Word16)]
compress = go Nothing
  where
    go _ [] = []
    go prev ((i, v) : rest)
      | Just v == prev = go prev rest
      | otherwise = (i, v) : go (Just v) rest

render :: String -> [(Int, Word16)] -> String
render version runs =
  unlines $
    [ "-- | Generated by scripts/GenUnicodeTable.hs from the Unicode " ++ version
    , "-- Character Database. Do not edit; to regenerate, see the header of"
    , "-- that script."
    , "--"
    , "-- The data is derived from the Unicode Character Database, © Unicode, Inc.,"
    , "-- under the Unicode License v3: see LICENSE.Unicode."
    , "module Lipstick.Internal.UnicodeTable (unicodeTableVersion, runStarts, runValues) where"
    , ""
    , "import Data.Array.Unboxed (UArray, listArray)"
    , "import Data.Word (Word16)"
    , ""
    , "unicodeTableVersion :: String"
    , "unicodeTableVersion = " ++ show version
    , ""
    , "-- | First code point of each run, ascending."
    , "runStarts :: UArray Int Int"
    , "runStarts = listArray (0, " ++ show (n - 1) ++ ")"
    ]
      ++ listLines (map (show . fst) runs)
      ++ [ ""
         , "-- | Packed properties of each run."
         , "runValues :: UArray Int Word16"
         , "runValues = listArray (0, " ++ show (n - 1) ++ ")"
         ]
      ++ listLines (map (show . snd) runs)
  where
    n = length runs
    listLines xs = case chunk 16 xs of
      [] -> ["  []"]
      c : cs -> ("  [ " ++ intercalate ", " c) : map (("  , " ++) . intercalate ", ") cs ++ ["  ]"]
    chunk k xs = case splitAt k xs of
      ([], _) -> []
      (a, b) -> a : chunk k b