Catalyst

将 HEX 颜色值转换为 RGB 颜色值

目前写的项目需要把hex颜色转换成rgb的形式,想起了之前写了一半就没动的elm/parser的博文,正好趁这个机会写完。

⚠️ 依旧是不完整笔记,只写用到的部分。

🎯 目标是将 #9fa0d7 这种形式的字符串提取出 RGB 的值:rgb(159,160,215)

安装方法:

elm install elm/parser

管道流程

包文档里提供的一个例子说明了 parser 的工作方法:

point : Parser Point
point =
  succeed Point
    |. symbol "("
    |. spaces
    |= float
    |. spaces
    |. symbol ","
    |. spaces
    |= float
    |. spaces
    |. symbol ")"

上面的|.|=之类的是处理数据的方法。

管道类型备注
|. Parser keep -> Parser ignore -> Parser keep解析并略过结果
|= Parser (a -> b) -> Parser a -> Parser b解析并保留结果,左侧类型要是Parser (a->b)
succeeda->Parser a直接输出内容
lazy(() -> Parser a) -> Parser a递归解析,例如在 parser 中包含 parser
andThen(a -> Parser b) -> Parser a -> Parser b解析一个之后接着解析另一个
problemString -> Parser a

所以point解析器表示的意思就是抽取(1,2)(33 , 4324)类似输入中的 xy 值。

可以直接用于解析的解析器有:

类型备注
int, float, number其中number可以同时解析不同类型的数字类型。
symbol符号,多用于(),
end检查是否到达字符串的终点。可以用于确保分析完了整个字符串
spaces包括<空格>, \n ,\r
keyword解析关键字:内容是连贯完整的: n+1 位不能是字母、数字或下划线
variable解析类似变量名的内容:有特定开头,内容过滤,排除保留字
lineComment ,multiComment解析注释(跳过内容)

对于我们的需求,颜色的有效输入格式是 \#[\d(a-f)(A-F)]{6}\。划分的时候两个两个一划并转换为十进制。大致流程如下:

type alias Color =
    { red : Int
    , green : Int
    , blue : Int
    }

hexToColor : Parser Int
hexToColor =
    succeed Color
            |. symbol "#"
            |= hexToInt
            |= hexToInt
            |= hexToInt
            |. end

忽略掉前面的#,然后实施 3 个hexToInt(每个处理两个字符),每个都保留传输到 Color 构造中。

逐字处理

由上,我们的第一行已经有了: |. symbol "#",但后面的就要自己处理了。看文档使用Chompers(咀嚼?感觉像是吃豆人那样一个一个处理并移除)。

名称类型备注
chompIf(Char -> Bool) -> Parser ()处理一个通过测试的字符
chompWhile(Char -> Bool) -> Parser ()处理连续的通过测试的字符
chompUntilString -> Parser ()处理直到碰到给定的字符
chompUntilEndOrString -> Parser ()处理直到碰到给定的字符或结尾
getChompedStringParser a -> Parser String字面意思
mapChompedString(String -> a -> b) -> Parser a -> Parser b字面意思

上文提到的symbol等也是chomper的一种,使用的话也会被getChompedString收到。

接着使用

hexToInt : Parser Int
hexToInt =
    Parser.map intFromHexString <|
        getChompedString <|
            chompIf Char.isHexDigit
                |. chompIf Char.isHexDigit
  1. chompIf处理两个字符
  2. getChompedString接受被 chomp 的字符
  3. 最后用 map 将 String 转换为 Int,其中用了一个 hex 转 int 的函数。

完整代码

点击查看
module Main exposing (main)

import Browser
import Html exposing (Html, div, input, text)
import Html.Attributes exposing (placeholder, type_)
import Html.Events exposing (onInput)
import Parser exposing (..)



-- MAIN


main : Program () Model Msg
main =
    Browser.sandbox { init = init, update = update, view = view }



-- MODEL


type alias Model =
    Maybe Color


type alias Color =
    { red : Int
    , green : Int
    , blue : Int
    }


init : Model
init =
    Nothing



-- UPDATE


type Msg
    = ToParse String


update : Msg -> Model -> Model
update msg model =
    case msg of
        ToParse input ->
            case run hexToColor input of
                Ok color ->
                    Just color

                Err _ ->
                    model



-- VIEW


view : Model -> Html Msg
view model =
    div []
        [ input [ type_ "text", placeholder "Input hex color", onInput ToParse ] []
        , div [] [ text (colorToString model) ]
        ]


hexToColor : Parser Color
hexToColor =
    let
        hexToInt : Parser Int
        hexToInt =
            Parser.map intFromHexString <|
                getChompedString <|
                    chompIf Char.isHexDigit
                        |. chompIf Char.isHexDigit
    in
    succeed Color
        |. symbol "#"
        |= hexToInt
        |= hexToInt
        |= hexToInt
        |. end


intFromHexString : String -> Int
intFromHexString hex =
    let
        singleHex c =
            if Char.isDigit c then
                Char.toCode c - Char.toCode '0'

            else
                Char.toCode c - Char.toCode 'A' + 10

        power i =
            List.product (List.repeat i 16)
    in
    String.toList (String.toUpper hex)
        |> List.indexedMap (\i item -> power (String.length hex - i - 1) * singleHex item)
        |> List.sum


colorToString : Maybe Color -> String
colorToString color =
    case color of
        Nothing ->
            "不合法的颜色值的输入。"

        Just p ->
            "rgb(" ++ String.fromInt p.red ++ "," ++ String.fromInt p.green ++ "," ++ String.fromInt p.blue ++ ")"

21.04.11 继续研究 Parser

写太短了不能变成一个新的博文,先放这里吧,以后写多了再独立出来

研究 HTML 套壳技术而写了一个笔记 APP BubbleNote,里面需要分析输入的笔记内容中属于标签(#标签#)的部分和属于链接的部分(https://github.com/)提取出来做样式,于是又回到了解析器的研究 ✍️

  • backtrackable : Parser a -> Parser a

可回溯的。例子:设定的语法中两个#中夹的是标签内容,但如果输入的内容只有一个#(作为普通的内容输入的),结果碰到#就分配到标签解析器,却因为少了一个#解析失败了……这时可以转回去使用别的解析器。

  • commit : a -> Parser a

作为返回的完结值?

  • loop : state -> (state -> Parser (Step state a)) -> Parser a

循环,有点复杂。

例子:有类型:

type Content
    = Text String
    | Tag String
    | Link String String

手上有几个解析器,分别是 tagParsertextParserlinkParser。要将一组数据解析为 List Content

loop的两个参数,state当前状态state -> Parser (Step state a) 相当于向下一个状态转移的函数。Step的定义为:

type Step state a
  = Loop state
  | Done a

它表示了下一步将采取的行动(是继续循环还是结束循环)。如果循环则将新的state赋予循环,如果结束循环则返回 a

在只解析一次的情况下,能写出:

oneParser : Parser Content
oneParser =
  oneOf [ linkParser, tagParser, textParser]

在我们的循环中则是使用解析器,如果还没结束则再次解析。仿写文档中的例子:

contentHelp : List Content -> Parser (Step (List Content) (List Content))
contentHelp cmds =
    let
        nextParser : Parser Content -> Parser (Step (List Content) (List Content))
        nextParser parser =
            succeed (\next -> Parser.Loop (next :: cmds))
                |= parser

        loopList : List (Parser (Step (List Content) (List Content)))
        loopList =
            List.map nextParser [ linkParser, Parser.backtrackable tagParser, textParser ]
    in
    oneOf <|
        List.append loopList
            [ succeed () |> Parser.map (\_ -> Parser.Done (List.reverse cmds)) ]
  1. 将所有解析器转换为Parser (Step (List Content) (List Content))类型,即如果执行了就添加结果到cmds 并进行下一个循环。
  2. 最后在解析器列表中添加一个用于结束的 Parser,参数是循环的结果(倒序的cmds,因为上文中使用::将每次解析的结果添加到了列表最前面)。
  3. 总体的Parser就是在这些分支中选一。