Haio 0.2.X
Homebrew Asset IO/CDN
Loading...
Searching...
No Matches
haio_cli_grammar.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "haio_codec.hpp"
4#include "haio_formats.hpp"
5
6#include <array>
7#include <boost/spirit/home/x3.hpp>
8#include <optional>
9#include <string>
10#include <string_view>
11
13
14namespace x3 = boost::spirit::x3;
15
16inline const auto paren = x3::string("\\(") | x3::string("\\)") | x3::string("(") | x3::string(")");
17inline const auto singleQuoted = x3::lexeme['\'' >> *(x3::char_ - '\'') >> '\''];
18inline const auto doubleQuoted = x3::lexeme['"' >> *(('\\' >> x3::char_) | (x3::char_ - '"')) >> '"'];
19inline const auto word = x3::lexeme[+(x3::char_ - x3::space - '"' - '\'' - '(' - ')')];
20inline const auto arg = x3::raw[singleQuoted | doubleQuoted | paren | word];
21inline const auto cmdline = x3::skip(x3::space)[*arg];
22
23struct Rule {
24 std::string_view name;
25 std::string_view expression;
26};
27
28struct Note {
29 std::string_view text;
30};
31
32inline constexpr std::array ebnfRules = {
33 Rule{"paren", R"ebnf("\‍(" | "\)" | "(" | ")")ebnf"},
34 Rule{"single-quoted", R"("'" , { any-character - "'" } , "'")"},
35 Rule{"double-quoted", R"ebnf(""" , { "\" , any-character | any-character - """ } , """)ebnf"},
36 Rule{"word", R"(non-empty-token-without-space-quote-or-paren)"},
37 Rule{"arg", "single-quoted | double-quoted | paren | word"},
38 Rule{"cmdline", "{ arg }"},
39};
40
41/**
42 * which option a word turned out to name, so that a spelling is recognised once, by
43 * the table below, rather than by whichever branch of the parser gets to it first.
44 */
45enum class Opt {
47 Crop,
49 Resize,
50 Radius,
55 Palette,
57};
58
59/**
60 * every option the convert line takes, and how many words follow each.
61 *
62 * it is a table because the same facts were written down three times: as an if in the
63 * parser, as a rule in the grammar this file documents, and as a line in the usage
64 * text. three copies of one list is three chances to disagree, and they did --
65 * "--size" was in the grammar and in the usage and in neither branch of the parser,
66 * and "--palette=cga" went looking for its value in the next word along, because the
67 * equals sign was only ever matched against the spelling the error message used.
68 *
69 * the count is the part that matters. reading an option is the same work whatever it
70 * means: take the words the table says it takes, from after an equals sign or from
71 * one along, and hand them to whoever knows what they are for. that last step is the
72 * only thing particular to the option, and it is all the parser's switch is left
73 * doing.
74 */
75struct Option {
77
78 /** every spelling it answers to; the first is the one the usage text shows */
79 std::array<std::string_view, 4> spellings;
80
81 /** how many words follow it */
82 int args = 1;
83
84 /** ... and whether it can do without them, which only -crop can */
85 bool optional = false;
86
87 /**
88 * whether it is one of the filters, which is what the grammar calls the options
89 * that may repeat between the source and the output. -size is not one: it belongs
90 * to the generator that has to follow it.
91 */
92 bool filter = true;
93
94 /** the name this option's rule has in the grammar */
95 std::string_view rule;
96
97 /** and the rule the words after it have to match */
98 std::string_view takes;
99
100 std::string_view help;
101};
102
103inline constexpr Option options[] = {
104 {.kind = Opt::Size, .spellings = {"-size"}, .filter = false,
105 .rule = "size-option", .takes = "size",
106 .help = "the size a generator is created at"},
107
108 {.kind = Opt::Crop, .spellings = {"-crop"}, .optional = true,
109 .rule = "crop-filter", .takes = "crop-geometry",
110 .help = "crop, written the way imagemagick writes it"},
111
112 {.kind = Opt::CropRect, .spellings = {"--crop"},
113 .rule = "crop-rect-filter", .takes = "rect",
114 .help = "crop by an explicit rectangle"},
115
116 {.kind = Opt::Resize, .spellings = {"-resize", "--resize", "--size"},
117 .rule = "resize-filter", .takes = "size",
118 .help = "resize, to a size or to a share such as 30pct"},
119
120 {.kind = Opt::Radius, .spellings = {"-radius", "--radius"},
121 .rule = "radius-filter", .takes = "integer",
122 .help = "round the corners away"},
123
124 {.kind = Opt::Format, .spellings = {"-format", "--format"},
125 .rule = "format-filter", .takes = "format-name",
126 .help = "the container to write, whatever the output is called"},
127
128 {.kind = Opt::PixFmt, .spellings = {"-pix_fmt", "--pix_fmt", "-pix_format", "--pix_format"},
129 .rule = "pix-fmt-filter", .takes = "colour-name",
130 .help = "the colour stored inside it, as ffmpeg names it"},
131
132 {.kind = Opt::Filter, .spellings = {"-filter", "--filter"},
133 .rule = "filter-setting", .takes = "dither-name",
134 .help = "how a colour that is not in the palette becomes one that is"},
135
136 {.kind = Opt::Limit, .spellings = {"-limit", "--limit"},
137 .rule = "limit-setting", .takes = "limit-spec",
138 .help = "keep only some of the palette's colours, and say which"},
139
140 {.kind = Opt::Palette, .spellings = {"-palete", "--palete", "-palette", "--palette"},
141 .rule = "palette-filter", .takes = "palette-spec",
142 .help = "fit the picture into a named palette"},
143
144 {.kind = Opt::Fx, .spellings = {"-fx"},
145 .rule = "fx-filter", .takes = "expression",
146 .help = "an expression, read and not yet run"},
147};
148
149/** an option, the spelling it arrived as, and whatever an equals sign carried with it */
150struct Word {
151 const Option* option = nullptr;
152 std::string_view spelling;
153 std::string_view value;
154 bool hasValue = false;
155};
156
157/**
158 * the option a word names, or nothing when it names none.
159 *
160 * "-resize=8x8" and "-resize 8x8" are one thing said two ways, so the equals sign is
161 * split off here rather than inside each option's own branch: whoever matched a
162 * spelling by hand had to remember this too, and one of them did not.
163 */
164constexpr std::optional<Word> optionOf(std::string_view token) {
165 const auto equals = token.find('=');
166 const auto head = token.substr(0, equals);
167
168 for (const auto& option : options) {
169 for (const auto spelling : option.spellings) {
170 if (spelling.empty()) break;
171 if (spelling != head) continue;
172 return Word{&option, spelling,
173 equals == std::string_view::npos ? std::string_view{} : token.substr(equals + 1),
174 equals != std::string_view::npos};
175 }
176 }
177 return std::nullopt;
178}
179
180/** the generators, which are a prefix on the source rather than an option of their own */
181inline constexpr std::string_view generatorKinds[] = {"xc", "canvas", "gradient", "radial-gradient"};
182
183namespace Detail {
184
185/** an alternation, broken across lines wherever one would run too wide to read */
186consteval void alternative(std::string& out, size_t& lineStart, std::string_view piece) {
187 constexpr size_t width = 58;
188 if (!out.empty()) {
189 if (out.size() - lineStart + piece.size() + 3 > width) {
190 out += "\n | ";
191 lineStart = out.size();
192 } else {
193 out += " | ";
194 }
195 }
196 out += piece;
197}
198
199consteval std::string_view quotedList(auto&& spellings) {
200 std::string out;
201 size_t lineStart = 0;
202 for (const auto spelling : spellings) {
203 if (spelling.empty()) break;
204 alternative(out, lineStart, "\"" + std::string(spelling) + "\"");
205 }
206 return std::define_static_string(out);
207}
208
209/** '( "-resize" | "--resize" | "--size" ) , size', which is an option's whole grammar */
210consteval std::string_view expressionOf(const Option& option) {
211 size_t count = 0;
212 for (const auto spelling : option.spellings) {
213 if (!spelling.empty()) count++;
214 }
215
216 std::string out{quotedList(option.spellings)};
217 if (count > 1) out = "( " + out + " )";
218
219 if (option.args > 0) {
220 out += option.optional ? " , [ " + std::string(option.takes) + " ]"
221 : " , " + std::string(option.takes);
222 }
223 return std::define_static_string(out);
224}
225
226/** one rule per option, off the table rather than beside it */
227consteval auto optionRules() {
228 std::array<Rule, std::size(options)> out{};
229 for (size_t i = 0; i < std::size(options); i++) {
230 out[i] = Rule{options[i].rule, expressionOf(options[i])};
231 }
232 return out;
233}
234
235/** the filters, named rather than spelled out, which is what { filter } stands for */
236consteval std::string_view filterAlternation() {
237 std::string out;
238 size_t lineStart = 0;
239 for (const auto& option : options) {
240 if (option.filter) alternative(out, lineStart, option.rule);
241 }
242 return std::define_static_string(out);
243}
244
245/**
246 * every name the Format enum answers to, straight off the enum.
247 *
248 * the list used to be written out by hand, which is how the grammar came to offer
249 * "etc1" and "rgb565" as containers long after they had stopped being any such thing.
250 * the parser has always asked the enum; now the page that documents it does too.
251 */
252consteval std::string_view formatAlternation() {
253 std::string out;
254 size_t lineStart = 0;
256 alternative(out, lineStart, "\"" + std::string(lowerOf(e)) + "\"");
257 }
258 return std::define_static_string(out);
259}
260
261/** and every name a colour answers to, which is the enum plus the borrowed spellings */
262consteval std::string_view colorAlternation() {
263 std::string out;
264 size_t lineStart = 0;
266 alternative(out, lineStart, "\"" + std::string(lowerOf(e)) + "\"");
267 }
268 for (const auto& alias : colorAliases) {
269 alternative(out, lineStart, "\"" + std::string(alias.spelling) + "\"");
270 }
271 return std::define_static_string(out);
272}
273
274template <size_t A, size_t B, size_t C>
275consteval auto joined(const std::array<Rule, A>& a, const std::array<Rule, B>& b, const std::array<Rule, C>& c) {
276 std::array<Rule, A + B + C> out{};
277 size_t at = 0;
278 for (const auto& rule : a) out[at++] = rule;
279 for (const auto& rule : b) out[at++] = rule;
280 for (const auto& rule : c) out[at++] = rule;
281 return out;
282}
283
284/** the shape of the line, which is the part no table can say */
285inline constexpr std::array structureRules = {
286 Rule{"convert-command", R"("convert" , source , { filter } , output)"},
287 Rule{"source", "file-spec | generator"},
288 Rule{"output", "file-spec"},
289 Rule{"file-spec", R"([ format-name , ":" ] , path)"},
290 Rule{"path", "arg"},
291 Rule{"generator", "size-option , generator-source"},
292 Rule{"generator-source", R"(generator-kind , ":" , generator-value)"},
293 Rule{"generator-kind", quotedList(generatorKinds)},
294 Rule{"filter", filterAlternation()},
295};
296
297/** and the words the options take, which is the part no table can say either */
298inline constexpr std::array leafRules = {
299 Rule{"format-name", formatAlternation()},
300 Rule{"colour-name", colorAlternation()},
301 Rule{"dither-name", R"("nearest" | "bayer" | "floyd" | "strict")"},
302 Rule{"limit-spec", R"(( "sort" | "spread" ) , ":" , integer)"},
303 Rule{"palette-spec", R"(palette-name , [ ":" , range , { "," , range } ])"},
304 Rule{"range", R"(integer , [ ".." , integer ])"},
305 Rule{"size", R"(integer , ( "x" | "X" ) , integer | integer , ( "%" | "pct" ))"},
306 Rule{"crop-geometry", R"(integer , ( "x" | "X" ) , integer , [ signed-integer , signed-integer ])"},
307 Rule{"rect", R"(integer , "," , integer , "," , integer , "," , integer)"},
308 Rule{"integer", "digit , { digit }"},
309 Rule{"signed-integer", R"(( "+" | "-" ) , integer)"},
310};
311
312}
313
314inline constexpr auto commandRules =
316
317inline constexpr std::array notes = {
318 Note{"`png:-` and `ppm:-` use stdin/stdout with an explicit format."},
319 Note{"an unknown prefix such as `foo:bar.png` is parsed as a normal path."},
320 Note{"every option takes its words after a space or after an `=`; the two are the same thing."},
321 Note{"`-size` is valid only when followed by a generator source, so an option written between the two is an error."},
322 Note{"generator sources are parsed but currently rejected during pipeline construction."},
323 Note{"`-fx` expressions are parsed but currently rejected during pipeline construction."},
324 Note{"`-crop` is the one option whose argument is optional; without geometry it creates a default crop token, which the current c++ pipeline rejects until a default crop operation is defined."},
325 Note{"`-filter` and `-limit` are settings: they wait for the operation that uses them, and a line that ends with one still waiting is an error."},
326 Note{"`-pix_fmt` names the colour stored inside the output, the way ffmpeg's option does; the container still comes from the output path or `-format`."},
327 Note{"naming a colour a container cannot store is an error rather than a silent conversion."},
328 Note{"`raw:out.bin` with a `-pix_fmt` writes the pixels themselves, which is the only way to ask for a colour with no container around it."},
329 Note{"the format and colour names above are generated from the enums, so they are whatever this build can actually do."},
330};
331
332}
#define HAIO_FOR_EACH_COLOR(e)
#define HAIO_FOR_EACH_FORMAT(e)
consteval std::string_view filterAlternation()
consteval auto joined(const std::array< Rule, A > &a, const std::array< Rule, B > &b, const std::array< Rule, C > &c)
constexpr std::array leafRules
consteval void alternative(std::string &out, size_t &lineStart, std::string_view piece)
consteval std::string_view formatAlternation()
constexpr std::array structureRules
consteval std::string_view colorAlternation()
consteval std::string_view expressionOf(const Option &option)
consteval std::string_view quotedList(auto &&spellings)
constexpr std::array ebnfRules
std::string_view expression
constexpr Option options[]
std::array< std::string_view, 4 > spellings
constexpr std::array notes
constexpr auto commandRules
constexpr std::string_view generatorKinds[]
constexpr std::optional< Word > optionOf(std::string_view token)
constexpr ColorAlias colorAliases[]
consteval std::string_view lowerOf(std::meta::info enumerator)