%% Copyright (c) 2017-2018, Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. %% Asciidoc User Guide 29 -module(asciideck_attributes_parser). -export([parse/1]). -type attributes() :: #{ %% The raw attribute list. 0 := binary(), %% Positional attributes. pos_integer() => binary(), %% Named attributes. binary() => binary() }. -export_type([attributes/0]). -define(IS_WS(C), (C =:= $\s) or (C =:= $\t)). -spec parse(binary()) -> attributes(). parse(Data) -> parse(Data, #{0 => Data}, 1). parse(<<>>, Attrs, _) -> Attrs; parse(Data, Attrs, Nth) -> case parse_attr(Data, <<>>) of {Value, Rest} when Nth =/= undefined -> parse(Rest, Attrs#{Nth => Value}, Nth + 1); {Name, Value, Rest} -> parse(Rest, Attrs#{Name => Value}, undefined) end. parse_attr(<<>>, Acc) -> {Acc, <<>>}; %% Skip preceding whitespace. parse_attr(<>, <<>>) when ?IS_WS(C) -> parse_attr(R, <<>>); %% Parse quoted positional attributes in their own function. parse_attr(<<$", R/bits>>, <<>>) -> parse_quoted_attr(R, <<>>); %% We have a named attribute, parse the value. parse_attr(<<$=, R/bits>>, Name) when Name =/= <<>> -> parse_attr_value(R, asciideck_block_parser:trim(Name, trailing), <<>>); %% We have a positional attribute. parse_attr(<<$,, R/bits>>, Value) -> {asciideck_block_parser:trim(Value, trailing), R}; %% Continue. parse_attr(<>, Acc) when C =/= $= -> parse_attr(R, <>). %% Get everything until the next double quote. parse_quoted_attr(<<$", R/bits>>, Acc) -> parse_quoted_attr_end(R, Acc); parse_quoted_attr(<<$\\, $", R/bits>>, Acc) -> parse_quoted_attr(R, <>); parse_quoted_attr(<>, Acc) -> parse_quoted_attr(R, <>). %% Skip the whitespace until the next comma or eof. parse_quoted_attr_end(<<>>, Value) -> {Value, <<>>}; parse_quoted_attr_end(<<$,, R/bits>>, Value) -> {Value, R}; parse_quoted_attr_end(<>, Value) when ?IS_WS(C) -> parse_quoted_attr_end(R, Value). parse_attr_value(<<>>, Name, Acc) -> {Name, Acc, <<>>}; %% Skip preceding whitespace. parse_attr_value(<>, Name, <<>>) when ?IS_WS(C) -> parse_attr_value(R, Name, <<>>); %% Parse quoted positional attributes in their own function. parse_attr_value(<<$", R/bits>>, Name, <<>>) -> {Value, Rest} = parse_quoted_attr(R, <<>>), {Name, Value, Rest}; %% Done. parse_attr_value(<<$,, R/bits>>, Name, Value) -> {Name, asciideck_block_parser:trim(Value, trailing), R}; %% Continue. parse_attr_value(<>, Name, Acc) -> parse_attr_value(R, Name, <>). -ifdef(TEST). attribute_0_test() -> #{0 := <<"Hello,world,width=\"50\"">>} = parse(<<"Hello,world,width=\"50\"">>), ok. parse_test() -> #{} = parse(<<>>), #{ 1 := <<"Hello">> } = parse(<<"Hello">>), #{ 1 := <<"quote">>, 2 := <<"Bertrand Russell">>, 3 := <<"The World of Mathematics (1956)">> } = parse(<<"quote, Bertrand Russell, The World of Mathematics (1956)">>), #{ 1 := <<"22 times">>, <<"backcolor">> := <<"#0e0e0e">>, <<"options">> := <<"noborders,wide">> } = parse(<<"\"22 times\", backcolor=\"#0e0e0e\", options=\"noborders,wide\"">>), #{ 1 := <<"A footnote, "with an image" image:smallnew.png[]">> } = parse(<<"A footnote, "with an image" image:smallnew.png[]">>), ok. -endif.