目前写的项目需要把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) |
succeed | a->Parser a | 直接输出内容 |
lazy | (() -> Parser a) -> Parser a | 递归解析,例如在 parser 中包含 parser |
andThen | (a -> Parser b) -> Parser a -> Parser b | 解析一个之后接着解析另一个 |
problem | String -> 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 () | 处理连续的通过测试的字符 |
chompUntil | String -> Parser () | 处理直到碰到给定的字符 |
chompUntilEndOr | String -> Parser () | 处理直到碰到给定的字符或结尾 |
getChompedString | Parser 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
- 用
chompIf
处理两个字符 - 用
getChompedString
接受被 chomp 的字符 - 最后用 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
手上有几个解析器,分别是 tagParser
、textParser
和 linkParser
。要将一组数据解析为 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)) ]
- 将所有解析器转换为
Parser (Step (List Content) (List Content))
类型,即如果执行了就添加结果到cmds
并进行下一个循环。 - 最后在解析器列表中添加一个用于结束的
Parser
,参数是循环的结果(倒序的cmds
,因为上文中使用::
将每次解析的结果添加到了列表最前面)。 - 总体的
Parser
就是在这些分支中选一。