I want to parse CSS color functions (for simplicity, all of the arguments are numbers between 0 and 255)
rgb(r,g,b)
rgba(r,g,b,a)
hsl(h,s,l)
hsla(h,s,l,a)
into
struct color
{
color(std::uint8_t r, std::uint8_t g, std::uint8_t b, std::uint8_t a) : red{r}, green{g}, blue{b}, alpha{a} {}
static color hsl(std::uint8_t h, std::uint8_t s, std::uint8_t l, std::uint8_t a) { ... }
std::uint8_t red;
std::uint8_t green;
std::uint8_t blue;
std::uint8_t alpha;
}
I have a working hsl
function implementation that converts the h, s, and l into rgb values.
I also have rule
s to handle the first two functions:
constexpr auto uint8 = uint_parser<std::uint8_t>{};
const auto color_rgb = rule<struct rgb, color>{"rgb"}
= lit("rgb") >> '(' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> ')' >> attr(255);
const auto color_rgba = rule<struct rgba, color>{"rgba"}
= lit("rgba") >> '(' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> ')';
This works because I have used
BOOST_FUSION_ADAPT_STRUCT(color,
red, green, blue, alpha)
The problem lies in the hsl
functions. I cannot do a second BOOST_FUSION_ADAPT_STRUCT
, so I thought of using semantic actions on the respective sequences of values where the semantic action would simply construct the color
from the sequence. Something like this:
const auto color_hsl = rule<struct hsl, color, true>{"hsl"}
= (lit("hsl") >> '(' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> ')' >> attr(255))[color::hsl];
This doesn't work, or I wouldn't be asking this question. Neither does Boost.Spirit.X3 have something in the sense of
[ qi::_val = phx::construct<color>(...), qi::_1, qi::_2, qi::_3, qi::_4) ];
It seems I want to do manually what BOOST_FUSION_ADAPT_STRUCT
does, but in a semantic action. Is this possible and how should I handle this? I know the attribute of the sequence should be an "fusion-vector" tuple-like entity with the four parsed values. I want to extract them and stuff them into color::hsl
to generate the rule's attribute.
Many hints apply here.
I cannot do a second BOOST_FUSION_ADAPT_STRUCT
Sure you can, see BOOST_FUSION_ADAPT_STRUCT_NAMED
In Qi this general form seem to apply:
[ qi::_val = phxfunction(qi::_0) ]
You could further simplify that by making your own actor type so you could just supply your "factory action": [ factory(&Foo::makeBar) ]
.
If you throw in an implementation of fusion::apply
¹ you can avoid ever dealing with the Fusion sequence manuallt
However, you might want to learn about this - very well hidden - attribute compatibility mode for semantic actions: BOOST_SPIRIT_ACTIONS_ALLOW_ATTR_COMPAT
. Buried in that change log:
Semantic actions now support attribute compatibility. This is a breaking change but #define
BOOST_SPIRIT_ACTIONS_ALLOW_ATTR_COMPAT
must be defined in order for the new behavior to kick in. By default, the old behavior is still in place.
You might get the behaviour you desire with much less tweaking.
X3 is really malleable. We can have that factory
helper described above in as little as:
auto factory = [](auto f) {
return [f](auto& ctx) {
x3::_val(ctx) = my_apply(f, x3::_attr(ctx));
};
};
I'll throw in a quick draft of my_apply
(for the boost::fusion::apply
described earlier):
namespace detail {
template <class F, class Sequence, std::size_t... I>
constexpr decltype(auto) apply_impl(F&& f, Sequence&& t, std::index_sequence<I...>)
{
return std::invoke(std::forward<F>(f), boost::fusion::at_c<I>(std::forward<Sequence>(t))...);
}
}
template <class F, class Sequence>
constexpr decltype(auto) my_apply(F&& f, Sequence&& t)
{
return detail::apply_impl(
std::forward<F>(f), std::forward<Sequence>(t),
std::make_index_sequence<typename boost::fusion::result_of::size<std::remove_reference_t<Sequence> >::type{}>{});
}
Now we can have the parser:
namespace parser {
using namespace x3;
constexpr auto uint8 = uint_parser<std::uint8_t>{};
auto factory = [](auto f) {
return [f](auto& ctx) {
x3::_val(ctx) = my_apply(f, x3::_attr(ctx));
};
};
const auto color_rgb = rule<struct rgb, ast::color>{"rgb"}
= lit("rgb") >> '(' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> x3::attr(255u) >> ')';
const auto color_rgba = rule<struct rgba, ast::color>{"rgba"}
= lit("rgba") >> '(' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> ')';
const auto color_hsl = rule<struct hsl, ast::color>{"hsl"}
= (lit("hsl") >> '(' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> attr(255u) >> ')') [factory(ast::color::hsl)];
const auto color = skip(space) [ color_rgba | color_rgb | color_hsl ];
}
And test it with:
int main() {
for (std::string const input : {
"rgb(1,2,3)",
"rgba(4,5,6,7)",
"hsl(8,9,10)" })
{
std::cout << " ----- Parsng " << std::quoted(input) << " --------\n";
auto begin = input.begin(), end = input.end();
ast::color result;
bool success = parse(begin, end, parser::color, result);
if (success) {
std::cout << "parsed: ";
std::cout << result << "\n";
} else {
std::cout << "failed\n";
}
if (begin != end) {
std::cout << "Remaining unparsed: " << std::quoted(std::string(begin, end)) << std::endl;
}
}
}
Prints:
----- Parsng "rgb(1,2,3)" --------
parsed: rgba(1,2,3,255)
----- Parsng "rgba(4,5,6,7)" --------
parsed: rgba(4,5,6,7)
----- Parsng "hsl(8,9,10)" --------
TODO: implement static ast::color ast::color::hsl(uint8_t, uint8_t, uint8_t, uint8_t)(8,9,10,255)
parsed: rgba(8,9,10,255)
#include <iostream>
#include <iomanip>
#include <boost/spirit/home/x3.hpp>
#include <boost/fusion/adapted/struct.hpp>
#include <boost/fusion/include/size.hpp>
namespace x3 = boost::spirit::x3;
namespace ast {
using std::uint8_t;
struct color {
uint8_t red, green, blue, alpha;
color(uint8_t r=0, uint8_t g=0, uint8_t b=0, uint8_t a=255) : red{r}, green{g}, blue{b}, alpha{a} {}
static color hsl(uint8_t h, uint8_t s, uint8_t l, uint8_t a) {
std::cerr << "TODO: implement " << __PRETTY_FUNCTION__ << "(" << 1*h << "," << 1*s << "," << 1*l << "," << 1*a << ")\n";
return {h,s,l,a}; }
};
static inline std::ostream& operator<<(std::ostream& os, color const& c) {
return os << "rgba(" << 1*c.red << "," << 1*c.green << "," << 1*c.blue << "," << 1*c.alpha << ")";
}
}
BOOST_FUSION_ADAPT_STRUCT(ast::color, red, green, blue, alpha);
namespace {
namespace detail {
template <class F, class Sequence, std::size_t... I>
constexpr decltype(auto) apply_impl(F&& f, Sequence&& t, std::index_sequence<I...>)
{
return std::invoke(std::forward<F>(f), boost::fusion::at_c<I>(std::forward<Sequence>(t))...);
}
}
template <class F, class Sequence>
constexpr decltype(auto) my_apply(F&& f, Sequence&& t)
{
return detail::apply_impl(
std::forward<F>(f), std::forward<Sequence>(t),
std::make_index_sequence<typename boost::fusion::result_of::size<std::remove_reference_t<Sequence> >::type{}>{});
}
}
namespace parser {
using namespace x3;
constexpr auto uint8 = uint_parser<std::uint8_t>{};
auto factory = [](auto f) {
return [f](auto& ctx) {
x3::_val(ctx) = my_apply(f, x3::_attr(ctx));
};
};
const auto color_rgb = rule<struct rgb, ast::color>{"rgb"}
= lit("rgb") >> '(' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> x3::attr(255u) >> ')';
const auto color_rgba = rule<struct rgba, ast::color>{"rgba"}
= lit("rgba") >> '(' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> ')';
const auto color_hsl = rule<struct hsl, ast::color>{"hsl"}
= (lit("hsl") >> '(' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> attr(255u) >> ')') [factory(ast::color::hsl)];
const auto color = skip(space) [ color_rgba | color_rgb | color_hsl ];
}
int main() {
for (std::string const input : {
"rgb(1,2,3)",
"rgba(4,5,6,7)",
"hsl(8,9,10)" })
{
std::cout << " ----- Parsng " << std::quoted(input) << " --------\n";
auto begin = input.begin(), end = input.end();
ast::color result;
bool success = parse(begin, end, parser::color, result);
if (success) {
std::cout << "parsed: ";
std::cout << result << "\n";
} else {
std::cout << "failed\n";
}
if (begin != end) {
std::cout << "Remaining unparsed: " << std::quoted(std::string(begin, end)) << std::endl;
}
}
}
¹ it seems not to exist. Of course you could copy to a std::tuple
and use std::apply
(also experimental)