New Tag Parser (#2303)

* refactor(color): Use enum class for color type

* Add testcases for tag parser

* Make tag parser a pull-style parser

Being able to parse single elements at a time gives us more fine-grained
error messages, we can also parse as much as possible and only stop
after an exception.

* fix(color): Parser did not check for invalid chars

* tag parser: First full implementation

* tag parser: Fix remaining failing tests

* tag parser: Replace old parser

* tag parser: Treat alignment as formatting tag

Makes the structure less complex and the alignment tags really are
formatting tags, they are structurally no different from the %{R} tag.

* tag parser: Cleanup type definitions

All type definitions for tags now live in tags/types.hpp, the parser.hpp
only contains the definitions necessary for actually calling the parser,
this shouldn't be included in many places (only places that actually do
parsing). But many places need the definitions for the tags themselves.

* Rename components/parser to tags/dispatch

* tag parser: Cleanup

* Add changelog
This commit is contained in:
Patrick Ziegler 2020-12-17 20:37:28 +01:00 committed by GitHub
parent c07cc09a5f
commit fd556525a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1588 additions and 505 deletions

View File

@ -8,6 +8,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Breaking
- We rewrote our tag parser. This shouldn't break anything, if you experience
any problems, please let us know.
The new parser now gives errors for certain invalid tags where the old parser
would just silently ignore them. Adding extra text to the end of a valid tag
now produces an error. For example, tags like `%{T-a}`, `%{T2abc}`, `%{rfoo}`,
and others will now start producing errors.
This does not affect you unless you are producing your own formatting tags
(for example in a script) and you are using one of these invalid tags.
### Added
- Warn states for the cpu, memory, fs, and battery modules.
([`#570`](https://github.com/polybar/polybar/issues/570),
@ -36,4 +46,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Increased precision for upload and download speeds: 0 decimal places for
KB/s (as before), 1 for MB/s and 2 for GB/s.
### Fixed
- Parser error if click command contained `}`
([`#2040`](https://github.com/polybar/polybar/issues/2040))
[Unreleased]: https://github.com/polybar/polybar/compare/3.5.2...HEAD

View File

@ -1,7 +1,7 @@
#pragma once
#include <cstdlib>
#include <atomic>
#include <cstdlib>
#include <mutex>
#include "common.hpp"
@ -9,8 +9,8 @@
#include "errors.hpp"
#include "events/signal_fwd.hpp"
#include "events/signal_receiver.hpp"
#include "utils/math.hpp"
#include "settings.hpp"
#include "utils/math.hpp"
#include "x11/types.hpp"
#include "x11/window.hpp"
@ -20,11 +20,14 @@ POLYBAR_NS
class config;
class connection;
class logger;
class parser;
class renderer;
class screen;
class taskqueue;
class tray_manager;
namespace tags {
class dispatch;
}
// }}}
/**
@ -38,7 +41,8 @@ inline double geom_format_to_pixels(std::string str, double max) {
if ((i = str.find(':')) != std::string::npos) {
std::string a = str.substr(0, i - 1);
std::string b = str.substr(i + 1);
return math_util::max<double>(0,math_util::percentage_to_value<double>(strtod(a.c_str(), nullptr), max) + strtod(b.c_str(), nullptr));
return math_util::max<double>(
0, math_util::percentage_to_value<double>(strtod(a.c_str(), nullptr), max) + strtod(b.c_str(), nullptr));
} else {
if (str.find('%') != std::string::npos) {
return math_util::percentage_to_value<double>(strtod(str.c_str(), nullptr), max);
@ -53,7 +57,8 @@ class bar : public xpp::event::sink<evt::button_press, evt::expose, evt::propert
public signal_receiver<SIGN_PRIORITY_BAR, signals::eventqueue::start, signals::ui::tick,
signals::ui::shade_window, signals::ui::unshade_window, signals::ui::dim_window
#if WITH_XCURSOR
, signals::ui::cursor_change
,
signals::ui::cursor_change
#endif
> {
public:
@ -61,7 +66,7 @@ class bar : public xpp::event::sink<evt::button_press, evt::expose, evt::propert
static make_type make(bool only_initialize_values = false);
explicit bar(connection&, signal_emitter&, const config&, const logger&, unique_ptr<screen>&&,
unique_ptr<tray_manager>&&, unique_ptr<parser>&&, unique_ptr<taskqueue>&&, bool only_initialize_values);
unique_ptr<tray_manager>&&, unique_ptr<tags::dispatch>&&, unique_ptr<taskqueue>&&, bool only_initialize_values);
~bar();
const bar_settings settings() const;
@ -108,7 +113,7 @@ class bar : public xpp::event::sink<evt::button_press, evt::expose, evt::propert
unique_ptr<screen> m_screen;
unique_ptr<tray_manager> m_tray;
unique_ptr<renderer> m_renderer;
unique_ptr<parser> m_parser;
unique_ptr<tags::dispatch> m_dispatch;
unique_ptr<taskqueue> m_taskqueue;
bar_settings m_opts{};

View File

@ -4,6 +4,7 @@
#include "common.hpp"
#include "components/types.hpp"
#include "tags/types.hpp"
POLYBAR_NS
using std::map;
@ -47,7 +48,7 @@ class builder {
void overline_close();
void underline(const rgba& color = rgba{});
void underline_close();
void control(controltag tag);
void control(tags::controltag tag);
void action(mousebtn index, string action);
void action(mousebtn btn, const modules::module_interface& module, string action, string data);
void action(mousebtn index, string action, const label_t& label);
@ -55,19 +56,18 @@ class builder {
void action_close();
protected:
void tag_open(syntaxtag tag, const string& value);
void tag_open(attribute attr);
void tag_close(syntaxtag tag);
void tag_close(attribute attr);
void tag_open(tags::syntaxtag tag, const string& value);
void tag_open(tags::attribute attr);
void tag_close(tags::syntaxtag tag);
void tag_close(tags::attribute attr);
private:
const bar_settings m_bar;
string m_output;
map<syntaxtag, int> m_tags{};
map<syntaxtag, string> m_colors{};
map<attribute, bool> m_attrs{};
map<tags::syntaxtag, int> m_tags{};
map<tags::syntaxtag, string> m_colors{};
map<tags::attribute, bool> m_attrs{};
int m_fontindex{0};
};

View File

@ -1,46 +0,0 @@
#pragma once
#include "common.hpp"
#include "errors.hpp"
#include "utils/color.hpp"
POLYBAR_NS
class signal_emitter;
enum class attribute;
enum class controltag;
enum class mousebtn;
struct bar_settings;
DEFINE_ERROR(parser_error);
DEFINE_CHILD_ERROR(unrecognized_token, parser_error);
DEFINE_CHILD_ERROR(unrecognized_attribute, parser_error);
DEFINE_CHILD_ERROR(unclosed_actionblocks, parser_error);
class parser {
public:
using make_type = unique_ptr<parser>;
static make_type make();
public:
explicit parser(signal_emitter& emitter);
void parse(const bar_settings& bar, string data);
protected:
void codeblock(string&& data, const bar_settings& bar);
size_t text(string&& data);
static rgba parse_color(const string& s, rgba fallback = rgba{0});
static int parse_fontindex(const string& s);
static attribute parse_attr(const char attr);
mousebtn parse_action_btn(const string& data);
static string parse_action_cmd(string&& data);
static controltag parse_control(const string& data);
private:
signal_emitter& m_sig;
vector<int> m_actions;
unique_ptr<parser> m_parser;
};
POLYBAR_NS_END

View File

@ -32,34 +32,20 @@ enum class edge { NONE = 0, TOP, BOTTOM, LEFT, RIGHT, ALL };
enum class alignment { NONE = 0, LEFT, CENTER, RIGHT };
enum class attribute { NONE = 0, UNDERLINE, OVERLINE };
enum class syntaxtag {
enum class mousebtn {
NONE = 0,
A, // mouse action
B, // background color
F, // foreground color
T, // font index
O, // pixel offset
R, // flip colors
o, // overline color
u, // underline color
P, // Polybar control tag
LEFT,
MIDDLE,
RIGHT,
SCROLL_UP,
SCROLL_DOWN,
DOUBLE_LEFT,
DOUBLE_MIDDLE,
DOUBLE_RIGHT,
// Terminator value, do not use
BTN_COUNT,
};
/**
* Values for polybar control tags
*
* %{P...} tags are tags for internal polybar control commands, they are not
* part of the public interface
*/
enum class controltag {
NONE = 0,
R, // Reset all open tags (B, F, T, o, u). Used at module edges
};
enum class mousebtn { NONE = 0, LEFT, MIDDLE, RIGHT, SCROLL_UP, SCROLL_DOWN, DOUBLE_LEFT, DOUBLE_MIDDLE, DOUBLE_RIGHT };
enum class strut {
LEFT = 0,
RIGHT,

View File

@ -2,8 +2,9 @@
#include "common.hpp"
#include "components/ipc.hpp"
#include "components/parser.hpp"
#include "components/types.hpp"
#include "tags/dispatch.hpp"
#include "tags/types.hpp"
#include "utils/functional.hpp"
POLYBAR_NS
@ -37,6 +38,7 @@ namespace signals {
explicit value_signal(void* data) : m_ptr(data) {}
explicit value_signal(ValueType&& data) : m_ptr(&data) {}
explicit value_signal(ValueType& data) : m_ptr(&data) {}
virtual ~value_signal() {}
@ -154,13 +156,13 @@ namespace signals {
struct offset_pixel : public detail::value_signal<offset_pixel, int> {
using base_type::base_type;
};
struct attribute_set : public detail::value_signal<attribute_set, attribute> {
struct attribute_set : public detail::value_signal<attribute_set, tags::attribute> {
using base_type::base_type;
};
struct attribute_unset : public detail::value_signal<attribute_unset, attribute> {
struct attribute_unset : public detail::value_signal<attribute_unset, tags::attribute> {
using base_type::base_type;
};
struct attribute_toggle : public detail::value_signal<attribute_toggle, attribute> {
struct attribute_toggle : public detail::value_signal<attribute_toggle, tags::attribute> {
using base_type::base_type;
};
struct action_begin : public detail::value_signal<action_begin, action> {
@ -172,7 +174,7 @@ namespace signals {
struct text : public detail::value_signal<text, string> {
using base_type::base_type;
};
struct control : public detail::value_signal<control, controltag> {
struct control : public detail::value_signal<control, tags::controltag> {
using base_type::base_type;
};
} // namespace parser

View File

@ -95,7 +95,7 @@ namespace modules {
m_builder->flush();
if (!m_cache.empty()) {
// Add a reset tag after the module
m_builder->control(controltag::R);
m_builder->control(tags::controltag::R);
m_cache += m_builder->flush();
}
m_changed = false;

41
include/tags/dispatch.hpp Normal file
View File

@ -0,0 +1,41 @@
#pragma once
#include "common.hpp"
#include "errors.hpp"
POLYBAR_NS
class signal_emitter;
enum class mousebtn;
struct bar_settings;
class logger;
namespace tags {
enum class attribute;
enum class controltag;
/**
* Calls into the tag parser to parse the given formatting string and then
* sends the right signals for each tag.
*/
class dispatch {
public:
using make_type = unique_ptr<dispatch>;
static make_type make();
explicit dispatch(signal_emitter& emitter, const logger& logger);
void parse(const bar_settings& bar, string data);
protected:
void text(string&& data);
void handle_action(mousebtn btn, bool closing, const string&& cmd);
private:
signal_emitter& m_sig;
vector<mousebtn> m_actions;
const logger& m_log;
};
} // namespace tags
POLYBAR_NS_END

159
include/tags/parser.hpp Normal file
View File

@ -0,0 +1,159 @@
#pragma once
#include "common.hpp"
#include "errors.hpp"
#include "tags/types.hpp"
POLYBAR_NS
namespace tags {
static constexpr char EOL = '\0';
class error : public application_error {
public:
using application_error::application_error;
explicit error(const string& msg) : application_error(msg), msg(msg) {}
/**
* Context string that contains the text region where the parser error
* happened.
*/
void set_context(const string& ctxt) {
msg.append(" (Context: '" + ctxt + "')");
}
virtual const char* what() const noexcept {
return msg.c_str();
}
private:
string msg;
};
#define DEFINE_INVALID_ERROR(class_name, name) \
class class_name : public error { \
public: \
explicit class_name(const string& val) : error("Invalid " name ": '" + val + "'") {} \
explicit class_name(const string& val, const string& what) \
: error("Invalid " name ": '" + val + "' (reason: '" + what + "')") {} \
}
DEFINE_INVALID_ERROR(color_error, "color");
DEFINE_INVALID_ERROR(font_error, "font index");
DEFINE_INVALID_ERROR(control_error, "control tag");
DEFINE_INVALID_ERROR(offset_error, "offset");
DEFINE_INVALID_ERROR(btn_error, "button id");
#undef DEFINE_INVALID_ERROR
class token_error : public error {
public:
explicit token_error(char token, char expected) : token_error(string{token}, string{expected}) {}
explicit token_error(char token, const string& expected) : token_error(string{token}, expected) {}
explicit token_error(const string& token, const string& expected)
: error("Expected '" + expected + "' but found '" +
(token.size() == 1 && token.at(0) == EOL ? "<End Of Line>" : token) + "'") {}
};
class unrecognized_tag : public error {
public:
explicit unrecognized_tag(char tag) : error("Unrecognized formatting tag '%{" + string{tag} + "}'") {}
};
class unrecognized_attr : public error {
public:
explicit unrecognized_attr(char attr) : error("Unrecognized attribute '" + string{attr} + "'") {}
};
/**
* Thrown when we expect the end of a tag (either } or a space in a compound
* tag.
*/
class tag_end_error : public error {
public:
explicit tag_end_error(char token)
: error("Expected the end of a tag ('}' or ' ') but found '" +
(token == EOL ? "<End Of Line>" : string{token}) + "'") {}
};
/**
* Recursive-descent parser for polybar's formatting tags.
*
* An input string is parsed into a list of elements, each element is either
* a piece of text or a single formatting tag.
*
* The elements can either be retrieved one-by-one with next_element() or all
* at once with parse().
*/
class parser {
public:
/**
* Resets the parser state and sets the new string to parse
*/
void set(const string&& input);
/**
* Whether a call to next_element() suceeds.
*/
bool has_next_element();
/**
* Parses at least one element (if available) and returns the first parsed
* element.
*/
element next_element();
/**
* Parses the remaining string and returns all parsed elements.
*/
format_string parse();
protected:
void parse_step();
bool has_next() const;
char next();
char peek() const;
void revert();
void consume(char c);
void consume_space();
void parse_tag();
void parse_single_tag_content();
color_value parse_color();
int parse_fontindex();
int parse_offset();
controltag parse_control();
std::pair<action_value, string> parse_action();
mousebtn parse_action_btn();
string parse_action_cmd();
attribute parse_attribute();
void push_char(char c);
void push_text(string&& text);
string get_tag_value();
private:
string input;
size_t pos = 0;
/**
* Buffers elements that have been parsed but not yet returned to the user.
*/
format_string buf{};
/**
* Index into buf so that we don't have to call vector.erase everytime.
*
* Only buf[buf_pos, buf.end()) contain valid elements
*/
size_t buf_pos;
};
} // namespace tags
POLYBAR_NS_END

118
include/tags/types.hpp Normal file
View File

@ -0,0 +1,118 @@
#pragma once
#include "common.hpp"
#include "components/types.hpp"
#include "utils/color.hpp"
POLYBAR_NS
namespace tags {
enum class attribute { NONE = 0, UNDERLINE, OVERLINE };
enum class attr_activation { NONE, ON, OFF, TOGGLE };
enum class syntaxtag {
A, // mouse action
B, // background color
F, // foreground color
T, // font index
O, // pixel offset
R, // flip colors
o, // overline color
u, // underline color
P, // Polybar control tag
l, // Left alignment
r, // Right alignment
c, // Center alignment
};
/**
* Values for polybar control tags
*
* %{P...} tags are tags for internal polybar control commands, they are not
* part of the public interface
*/
enum class controltag {
NONE = 0,
R, // Reset all open tags (B, F, T, o, u). Used at module edges
};
enum class color_type { RESET = 0, COLOR };
struct color_value {
/**
* ARGB color.
*
* Only used if type == COLOR
*/
rgba val{};
color_type type;
};
/**
* Stores information about an action
*
* The actual command string is stored in element.data
*/
struct action_value {
/**
* NONE is only allowed for closing tags
*/
mousebtn btn;
bool closing;
};
enum class tag_type { ATTR, FORMAT };
union tag_subtype {
syntaxtag format;
attr_activation activation;
};
struct tag {
tag_type type;
tag_subtype subtype;
union {
/**
* Used for 'F', 'G', 'o', 'u' formatting tags.
*/
color_value color;
/**
* For for 'A' tags
*/
action_value action;
/**
* For for 'T' tags
*/
int font;
/**
* For for 'O' tags
*/
int offset;
/**
* For for 'P' tags
*/
controltag ctrl;
/**
* For attribute activations ((-|+|!)(o|u))
*/
attribute attr;
};
};
struct element {
element(){};
element(const string&& text) : data{std::move(text)}, is_tag{false} {};
string data{};
tag tag_data{};
bool is_tag{false};
};
using format_string = vector<element>;
} // namespace tags
POLYBAR_NS_END

View File

@ -11,10 +11,10 @@ POLYBAR_NS
*/
class rgba {
public:
enum color_type { NONE, ARGB, ALPHA_ONLY };
enum class type { NONE = 0, ARGB, ALPHA_ONLY };
explicit rgba();
explicit rgba(uint32_t value, color_type type = ARGB);
explicit rgba(uint32_t value, type type = type::ARGB);
explicit rgba(string hex);
operator string() const;
@ -22,7 +22,7 @@ class rgba {
bool operator==(const rgba& other) const;
uint32_t value() const;
color_type type() const;
type type() const;
double alpha_d() const;
double red_d() const;
@ -56,7 +56,7 @@ class rgba {
*
* Cannot be const because we have to assign to it in the constructor and initializer lists are not possible.
*/
color_type m_type{NONE};
enum type m_type { type::NONE };
};
namespace color_util {

View File

@ -3,7 +3,6 @@
#include <algorithm>
#include "components/config.hpp"
#include "components/parser.hpp"
#include "components/renderer.hpp"
#include "components/screen.hpp"
#include "components/taskqueue.hpp"
@ -11,6 +10,7 @@
#include "drawtypes/label.hpp"
#include "events/signal.hpp"
#include "events/signal_emitter.hpp"
#include "tags/dispatch.hpp"
#include "utils/bspwm.hpp"
#include "utils/color.hpp"
#include "utils/factory.hpp"
@ -47,7 +47,7 @@ bar::make_type bar::make(bool only_initialize_values) {
logger::make(),
screen::make(),
tray_manager::make(),
parser::make(),
tags::dispatch::make(),
taskqueue::make(),
only_initialize_values);
// clang-format on
@ -59,7 +59,7 @@ bar::make_type bar::make(bool only_initialize_values) {
* TODO: Break out all tray handling
*/
bar::bar(connection& conn, signal_emitter& emitter, const config& config, const logger& logger,
unique_ptr<screen>&& screen, unique_ptr<tray_manager>&& tray_manager, unique_ptr<parser>&& parser,
unique_ptr<screen>&& screen, unique_ptr<tray_manager>&& tray_manager, unique_ptr<tags::dispatch>&& dispatch,
unique_ptr<taskqueue>&& taskqueue, bool only_initialize_values)
: m_connection(conn)
, m_sig(emitter)
@ -67,7 +67,7 @@ bar::bar(connection& conn, signal_emitter& emitter, const config& config, const
, m_log(logger)
, m_screen(forward<decltype(screen)>(screen))
, m_tray(forward<decltype(tray_manager)>(tray_manager))
, m_parser(forward<decltype(parser)>(parser))
, m_dispatch(forward<decltype(dispatch)>(dispatch))
, m_taskqueue(forward<decltype(taskqueue)>(taskqueue)) {
string bs{m_conf.section()};
@ -361,8 +361,8 @@ void bar::parse(string&& data, bool force) {
m_renderer->begin(rect);
try {
m_parser->parse(settings(), data);
} catch (const parser_error& err) {
m_dispatch->parse(settings(), data);
} catch (const exception& err) {
m_log.err("Failed to parse contents (reason: %s)", err.what());
}

View File

@ -9,6 +9,8 @@
#include "utils/time.hpp"
POLYBAR_NS
using namespace tags;
builder::builder(const bar_settings& bar) : m_bar(bar) {
reset();
}
@ -18,7 +20,6 @@ void builder::reset() {
* the map
*/
m_tags.clear();
m_tags[syntaxtag::NONE] = 0;
m_tags[syntaxtag::A] = 0;
m_tags[syntaxtag::B] = 0;
m_tags[syntaxtag::F] = 0;
@ -451,8 +452,6 @@ void builder::tag_open(syntaxtag tag, const string& value) {
m_tags[tag]++;
switch (tag) {
case syntaxtag::NONE:
break;
case syntaxtag::A:
append("%{A" + value + "}");
break;
@ -480,6 +479,15 @@ void builder::tag_open(syntaxtag tag, const string& value) {
case syntaxtag::P:
append("%{P" + value + "}");
break;
case syntaxtag::l:
append("%{l}");
break;
case syntaxtag::c:
append("%{c}");
break;
case syntaxtag::r:
append("%{r}");
break;
}
}
@ -534,10 +542,12 @@ void builder::tag_close(syntaxtag tag) {
case syntaxtag::o:
append("%{o-}");
break;
case syntaxtag::NONE:
case syntaxtag::R:
case syntaxtag::P:
case syntaxtag::O:
case syntaxtag::l:
case syntaxtag::c:
case syntaxtag::r:
break;
}
}

View File

@ -1,323 +0,0 @@
#include "components/parser.hpp"
#include <cassert>
#include "components/types.hpp"
#include "events/signal.hpp"
#include "events/signal_emitter.hpp"
#include "settings.hpp"
#include "utils/color.hpp"
#include "utils/factory.hpp"
#include "utils/memory.hpp"
#include "utils/string.hpp"
POLYBAR_NS
using namespace signals::parser;
/**
* Create instance
*/
parser::make_type parser::make() {
return factory_util::unique<parser>(signal_emitter::make());
}
/**
* Construct parser instance
*/
parser::parser(signal_emitter& emitter) : m_sig(emitter) {}
/**
* Process input string
*/
void parser::parse(const bar_settings& bar, string data) {
while (!data.empty()) {
size_t pos{string::npos};
if (data.compare(0, 2, "%{") == 0 && (pos = data.find('}')) != string::npos) {
codeblock(data.substr(2, pos - 2), bar);
data.erase(0, pos + 1);
} else if ((pos = data.find("%{")) != string::npos) {
data.erase(0, text(data.substr(0, pos)));
} else {
data.erase(0, text(data.substr(0)));
}
}
if (!m_actions.empty()) {
throw unclosed_actionblocks(to_string(m_actions.size()) + " unclosed action block(s)");
}
}
/**
* Process contents within tag blocks, i.e: %{...}
*/
void parser::codeblock(string&& data, const bar_settings& bar) {
size_t pos;
while (data.length()) {
data = string_util::ltrim(move(data), ' ');
if (data.empty()) {
break;
}
char tag{data[0]};
/*
* Contains the string from the current position to the next space or
* closing curly bracket (})
*
* This may be unsuitable for some tags (e.g. action tag) to use
* These MUST set value to the actual string they parsed from the beginning
* of data (or a string with the same length). The length of value is used
* to progress the cursor further.
*
* example:
*
* data = A1:echo "test": ...}
*
* erase(0,1)
* -> data = 1:echo "test": ...}
*
* case 'A', parse_action_cmd
* -> value = echo "test"
*
* Padding value
* -> value = echo "test"0::
*
* erase(0, value.length())
* -> data = ...}
*
*/
string value;
// Remove the tag
data.erase(0, 1);
if ((pos = data.find_first_of(" }")) != string::npos) {
value = data.substr(0, pos);
} else {
value = data;
}
switch (tag) {
case 'B':
m_sig.emit(change_background{parse_color(value, bar.background)});
break;
case 'F':
m_sig.emit(change_foreground{parse_color(value, bar.foreground)});
break;
case 'T':
m_sig.emit(change_font{parse_fontindex(value)});
break;
case 'U':
m_sig.emit(change_underline{parse_color(value, bar.underline.color)});
m_sig.emit(change_overline{parse_color(value, bar.overline.color)});
break;
case 'u':
m_sig.emit(change_underline{parse_color(value, bar.underline.color)});
break;
case 'o':
m_sig.emit(change_overline{parse_color(value, bar.overline.color)});
break;
case 'R':
m_sig.emit(reverse_colors{});
break;
case 'O':
m_sig.emit(offset_pixel{static_cast<int>(std::strtol(value.c_str(), nullptr, 10))});
break;
case 'l':
m_sig.emit(change_alignment{alignment::LEFT});
break;
case 'c':
m_sig.emit(change_alignment{alignment::CENTER});
break;
case 'r':
m_sig.emit(change_alignment{alignment::RIGHT});
break;
case '+':
m_sig.emit(attribute_set{parse_attr(value[0])});
break;
case '-':
m_sig.emit(attribute_unset{parse_attr(value[0])});
break;
case '!':
m_sig.emit(attribute_toggle{parse_attr(value[0])});
break;
case 'A': {
bool has_btn_id = (data[0] != ':');
if (isdigit(data[0]) || !has_btn_id) {
value = parse_action_cmd(data.substr(has_btn_id ? 1 : 0));
mousebtn btn = parse_action_btn(data);
m_actions.push_back(static_cast<int>(btn));
// Unescape colons inside command before sending it to the renderer
auto cmd = string_util::replace_all(value, "\\:", ":");
m_sig.emit(action_begin{action{btn, cmd}});
/*
* make sure value has the same length as the inside of the action
* tag which is btn_id + ':' + value + ':'
*/
if (has_btn_id) {
value += "0";
}
value += "::";
} else if (!m_actions.empty()) {
m_sig.emit(action_end{parse_action_btn(value)});
m_actions.pop_back();
}
break;
}
// Internal Polybar control tags
case 'P':
m_sig.emit(control{parse_control(value)});
break;
default:
throw unrecognized_token("Unrecognized token '" + string{tag} + "'");
}
if (!data.empty()) {
// Remove the parsed string from data
data.erase(0, !value.empty() ? value.length() : 1);
}
}
}
/**
* Process text contents
*/
size_t parser::text(string&& data) {
#ifdef DEBUG_WHITESPACE
string::size_type p;
while ((p = data.find(' ')) != string::npos) {
data.replace(p, 1, "-"s);
}
#endif
m_sig.emit(signals::parser::text{forward<string>(data)});
return data.size();
}
/**
* Process color hex string and convert it to the correct value
*/
rgba parser::parse_color(const string& s, rgba fallback) {
if (!s.empty() && s[0] != '-') {
rgba ret = rgba{s};
if (!ret.has_color() || ret.type() == rgba::ALPHA_ONLY) {
logger::make().warn(
"Invalid color in formatting tag detected: \"%s\", using fallback \"%s\". This is an issue with one of your "
"formatting tags. If it is not, please report this as a bug.",
s, static_cast<string>(fallback));
return fallback;
}
return ret;
}
return fallback;
}
/**
* Process font index and convert it to the correct value
*/
int parser::parse_fontindex(const string& s) {
if (s.empty() || s[0] == '-') {
return 0;
}
try {
return std::stoul(s, nullptr, 10);
} catch (const std::invalid_argument& err) {
return 0;
}
}
/**
* Process attribute token and convert it to the correct value
*/
attribute parser::parse_attr(const char attr) {
switch (attr) {
case 'o':
return attribute::OVERLINE;
case 'u':
return attribute::UNDERLINE;
default:
throw unrecognized_token("Unrecognized attribute '" + string{attr} + "'");
}
}
/**
* Process action button token and convert it to the correct value
*/
mousebtn parser::parse_action_btn(const string& data) {
if (data[0] == ':') {
return mousebtn::LEFT;
} else if (isdigit(data[0])) {
return static_cast<mousebtn>(data[0] - '0');
} else if (!m_actions.empty()) {
return static_cast<mousebtn>(m_actions.back());
} else {
return mousebtn::NONE;
}
}
/**
* Process action command string
*
* data is the action cmd surrounded by unescaped colons followed by an
* arbitrary string
*
* Returns everything inside the unescaped colons as is
*/
string parser::parse_action_cmd(string&& data) {
if (data[0] != ':') {
return "";
}
size_t end{1};
while ((end = data.find(':', end)) != string::npos && data[end - 1] == '\\') {
end++;
}
if (end == string::npos) {
return "";
}
return data.substr(1, end - 1);
}
controltag parser::parse_control(const string& data) {
if (data.length() != 1) {
return controltag::NONE;
}
switch (data[0]) {
case 'R':
return controltag::R;
break;
default:
return controltag::NONE;
break;
}
}
POLYBAR_NS_END

View File

@ -379,11 +379,12 @@ void renderer::flush(alignment a) {
double fsize = std::max(5.0, std::min(std::abs(overflow), 30.0));
m_log.trace("renderer: Drawing falloff (pos=%g, size=%g, overflow=%g)", visible_width - fsize, fsize, overflow);
m_context->save();
*m_context << cairo::translate{(double) m_rect.x, (double) m_rect.y};
*m_context << cairo::translate{(double)m_rect.x, (double)m_rect.y};
*m_context << cairo::abspos{0.0, 0.0};
*m_context << cairo::rect{x + visible_width - fsize, y, fsize, h};
m_context->clip(true);
*m_context << cairo::linear_gradient{x + visible_width - fsize, y, x + visible_width, y, {rgba{0x00000000}, rgba{0xFF000000}}};
*m_context << cairo::linear_gradient{
x + visible_width - fsize, y, x + visible_width, y, {rgba{0x00000000}, rgba{0xFF000000}}};
m_context->paint(0.25);
m_context->restore();
}
@ -480,8 +481,7 @@ double renderer::block_x(alignment a) const {
* The center block can be moved to the left if the right block is too large
*/
base_pos = std::min(base_pos, max_pos - block_w(a) / 2.0);
}
else {
} else {
base_pos = (min_pos + max_pos) / 2.0;
}
@ -594,7 +594,7 @@ void renderer::fill_background() {
* Fill overline color
*/
void renderer::fill_overline(double x, double w) {
if (m_bar.overline.size && m_attr.test(static_cast<int>(attribute::OVERLINE))) {
if (m_bar.overline.size && m_attr.test(static_cast<int>(tags::attribute::OVERLINE))) {
m_log.trace_x("renderer: overline(x=%f, w=%f)", x, w);
m_context->save();
*m_context << m_comp_ol;
@ -609,7 +609,7 @@ void renderer::fill_overline(double x, double w) {
* Fill underline color
*/
void renderer::fill_underline(double x, double w) {
if (m_bar.underline.size && m_attr.test(static_cast<int>(attribute::UNDERLINE))) {
if (m_bar.underline.size && m_attr.test(static_cast<int>(tags::attribute::UNDERLINE))) {
m_log.trace_x("renderer: underline(x=%f, w=%f)", x, w);
m_context->save();
*m_context << m_comp_ul;
@ -877,7 +877,7 @@ bool renderer::on(const signals::parser::control& evt) {
auto ctrl = evt.cast();
switch (ctrl) {
case controltag::R:
case tags::controltag::R:
m_bg = m_bar.background;
m_fg = m_bar.foreground;
m_ul = m_bar.underline.color;
@ -886,7 +886,7 @@ bool renderer::on(const signals::parser::control& evt) {
m_attr.reset();
break;
case controltag::NONE:
case tags::controltag::NONE:
break;
}

171
src/tags/dispatch.cpp Normal file
View File

@ -0,0 +1,171 @@
#include "tags/dispatch.hpp"
#include <algorithm>
#include "events/signal.hpp"
#include "events/signal_emitter.hpp"
#include "settings.hpp"
#include "tags/parser.hpp"
#include "utils/color.hpp"
#include "utils/factory.hpp"
POLYBAR_NS
using namespace signals::parser;
namespace tags {
static rgba get_color(tags::color_value c, rgba fallback) {
if (c.type == tags::color_type::RESET) {
return fallback;
} else {
return c.val;
}
}
/**
* Create instance
*/
dispatch::make_type dispatch::make() {
return factory_util::unique<dispatch>(signal_emitter::make(), logger::make());
}
/**
* Construct parser instance
*/
dispatch::dispatch(signal_emitter& emitter, const logger& logger) : m_sig(emitter), m_log(logger) {}
/**
* Process input string
*/
void dispatch::parse(const bar_settings& bar, string data) {
tags::parser p;
p.set(std::move(data));
while (p.has_next_element()) {
tags::element el;
try {
el = p.next_element();
} catch (const tags::error& e) {
m_log.err("Parser error (reason: %s)", e.what());
continue;
}
if (el.is_tag) {
switch (el.tag_data.type) {
case tags::tag_type::FORMAT:
switch (el.tag_data.subtype.format) {
case tags::syntaxtag::A:
handle_action(el.tag_data.action.btn, el.tag_data.action.closing, std::move(el.data));
break;
case tags::syntaxtag::B:
m_sig.emit(change_background{get_color(el.tag_data.color, bar.background)});
break;
case tags::syntaxtag::F:
m_sig.emit(change_foreground{get_color(el.tag_data.color, bar.foreground)});
break;
case tags::syntaxtag::T:
m_sig.emit(change_font{el.tag_data.font});
break;
case tags::syntaxtag::O:
m_sig.emit(offset_pixel{el.tag_data.offset});
break;
case tags::syntaxtag::R:
m_sig.emit(reverse_colors{});
break;
case tags::syntaxtag::o:
m_sig.emit(change_overline{get_color(el.tag_data.color, bar.overline.color)});
break;
case tags::syntaxtag::u:
m_sig.emit(change_underline{get_color(el.tag_data.color, bar.underline.color)});
break;
case tags::syntaxtag::P:
m_sig.emit(control{el.tag_data.ctrl});
break;
case tags::syntaxtag::l:
m_sig.emit(change_alignment{alignment::LEFT});
break;
case tags::syntaxtag::r:
m_sig.emit(change_alignment{alignment::RIGHT});
break;
case tags::syntaxtag::c:
m_sig.emit(change_alignment{alignment::CENTER});
break;
default:
throw runtime_error(
"Unrecognized tag format: " + to_string(static_cast<int>(el.tag_data.subtype.format)));
}
break;
case tags::tag_type::ATTR:
tags::attribute act = el.tag_data.attr;
switch (el.tag_data.subtype.activation) {
case tags::attr_activation::ON:
m_sig.emit(attribute_set{act});
break;
case tags::attr_activation::OFF:
m_sig.emit(attribute_unset{act});
break;
case tags::attr_activation::TOGGLE:
m_sig.emit(attribute_toggle{act});
break;
default:
throw runtime_error("Unrecognized attribute activation: " +
to_string(static_cast<int>(el.tag_data.subtype.activation)));
}
break;
}
} else {
text(std::move(el.data));
}
}
if (!m_actions.empty()) {
throw runtime_error(to_string(m_actions.size()) + " unclosed action block(s)");
}
}
/**
* Process text contents
*/
void dispatch::text(string&& data) {
#ifdef DEBUG_WHITESPACE
string::size_type p;
while ((p = data.find(' ')) != string::npos) {
data.replace(p, 1, "-"s);
}
#endif
m_sig.emit(signals::parser::text{std::move(data)});
}
void dispatch::handle_action(mousebtn btn, bool closing, const string&& cmd) {
if (closing) {
if (btn == mousebtn::NONE) {
if (!m_actions.empty()) {
btn = m_actions.back();
m_actions.pop_back();
} else {
m_log.err("parser: Closing action tag without matching tag");
}
} else {
auto it = std::find(m_actions.crbegin(), m_actions.crend(), btn);
if (it == m_actions.rend()) {
m_log.err("parser: Closing action tag for button %d without matching opening tag", static_cast<int>(btn));
} else {
/*
* We can't erase with a reverse iterator, we have to get
* the forward iterator first
* https://stackoverflow.com/a/1830240/5363071
*/
m_actions.erase(std::next(it).base());
}
}
m_sig.emit(action_end{btn});
} else {
m_actions.push_back(btn);
m_sig.emit(action_begin{action{btn, std::move(cmd)}});
}
}
} // namespace tags
POLYBAR_NS_END

513
src/tags/parser.cpp Normal file
View File

@ -0,0 +1,513 @@
#include "tags/parser.hpp"
#include <cassert>
#include <cctype>
POLYBAR_NS
namespace tags {
bool parser::has_next_element() {
return buf_pos < buf.size() || has_next();
}
element parser::next_element() {
if (!has_next_element()) {
throw std::runtime_error("tag parser: No next element");
}
if (buf_pos >= buf.size()) {
parse_step();
}
if (buf_pos >= buf.size()) {
throw std::runtime_error("tag parser: No next element. THIS IS A BUG. (Context: '" + input + "')");
}
element e = buf[buf_pos];
buf_pos++;
if (buf_pos == buf.size()) {
buf.clear();
buf_pos = 0;
}
return e;
}
format_string parser::parse() {
format_string parsed;
while (has_next_element()) {
parsed.push_back(next_element());
}
return parsed;
}
/**
* Performs a single parse step.
*
* This means it will parse text until the next tag is reached or it will
* parse an entire %{...} tag.
*/
void parser::parse_step() {
char c;
/*
* If we have already parsed text, we can stop if we reach a tag.
*/
bool text_parsed = false;
size_t start_pos = pos;
try {
while ((c = next())) {
// TODO here we could think about how to escape an action tag
if (c == '%' && has_next() && peek() == '{') {
/*
* If we have already parsed text, encountering a tag opening means
* we can stop parsing now because we parsed at least one entire
* element (the text up to the beginning of the tag).
*/
if (text_parsed) {
// Put back the '%'
revert();
break;
}
consume('{');
consume_space();
parse_tag();
break;
} else {
push_char(c);
text_parsed = true;
}
}
} catch (error& e) {
e.set_context(input.substr(start_pos, pos - start_pos));
throw;
}
}
void parser::set(const string&& input) {
this->input = std::move(input);
pos = 0;
buf.clear();
buf_pos = 0;
}
bool parser::has_next() const {
return pos < input.size();
}
char parser::next() {
char c = peek();
pos++;
return c;
}
char parser::peek() const {
if (!has_next()) {
return EOL;
}
return input[pos];
}
/**
* Puts back a single character in the input string.
*/
void parser::revert() {
assert(pos > 0);
pos--;
}
void parser::consume(char c) {
char n = next();
if (n != c) {
throw tags::token_error(n, c);
}
}
void parser::consume_space() {
while (peek() == ' ') {
next();
}
}
/**
* Parses an entire %{....} tag.
*
* '%' and '{' were already consumed and we are currently on the first character
* inside the tag.
* At the end of this method, we should be on the closing '}' character (not
* yet consumed).
*/
void parser::parse_tag() {
if (!has_next()) {
throw token_error(EOL, "Formatting tag content");
}
while (has_next()) {
parse_single_tag_content();
int p = peek();
if (p != ' ' && p != '}') {
throw tag_end_error(p);
} else {
/**
* Consume whitespace between elements inside the tag
*/
consume_space();
if (peek() == '}') {
consume('}');
break;
}
}
}
}
/**
* Parses a single element inside a formatting tag.
*
* For example it would parse the foreground part of the following tag:
*
* %{F#ff0000 B#ff0000}
* ^ ^
* | - Pointer at the end
* |
* - Pointer at the start
*/
void parser::parse_single_tag_content() {
char c = next();
/**
* %{U...} is a special case because it produces over and underline tags.
*/
if (c == 'U') {
element e{};
e.is_tag = true;
e.tag_data.type = tag_type::FORMAT;
e.tag_data.subtype.format = syntaxtag::u;
e.tag_data.color = parse_color();
buf.emplace_back(e);
e.tag_data.subtype.format = syntaxtag::o;
buf.emplace_back(e);
return;
}
tag_type type;
tag_subtype sub;
switch (c) {
// clang-format off
case 'B': sub.format = syntaxtag::B; break;
case 'F': sub.format = syntaxtag::F; break;
case 'u': sub.format = syntaxtag::u; break;
case 'o': sub.format = syntaxtag::o; break;
case 'T': sub.format = syntaxtag::T; break;
case 'R': sub.format = syntaxtag::R; break;
case 'O': sub.format = syntaxtag::O; break;
case 'P': sub.format = syntaxtag::P; break;
case 'A': sub.format = syntaxtag::A; break;
case 'l': sub.format = syntaxtag::l; break;
case 'c': sub.format = syntaxtag::c; break;
case 'r': sub.format = syntaxtag::r; break;
case '+': sub.activation = attr_activation::ON; break;
case '-': sub.activation = attr_activation::OFF; break;
case '!': sub.activation = attr_activation::TOGGLE; break;
// clang-format on
default:
throw unrecognized_tag(c);
}
switch (c) {
case 'B':
case 'F':
case 'u':
case 'o':
case 'T':
case 'R':
case 'O':
case 'P':
case 'A':
case 'l':
case 'c':
case 'r':
type = tag_type::FORMAT;
break;
case '+':
case '-':
case '!':
type = tag_type::ATTR;
break;
default:
throw unrecognized_tag(c);
}
tag tag_data{};
tag_data.type = type;
tag_data.subtype = sub;
element e{};
e.is_tag = true;
switch (c) {
case 'B':
case 'F':
case 'u':
case 'o':
tag_data.color = parse_color();
break;
case 'T':
tag_data.font = parse_fontindex();
break;
case 'O':
tag_data.offset = parse_offset();
break;
case 'P':
tag_data.ctrl = parse_control();
break;
case 'A':
std::tie(tag_data.action, e.data) = parse_action();
break;
case '+':
case '-':
case '!':
tag_data.attr = parse_attribute();
break;
}
e.tag_data = tag_data;
buf.emplace_back(e);
}
color_value parser::parse_color() {
string s = get_tag_value();
color_value ret;
if (s.empty() || s == "-") {
ret.type = color_type::RESET;
} else {
rgba c{s};
if (!c.has_color()) {
throw color_error(s);
}
ret.type = color_type::COLOR;
ret.val = c;
}
return ret;
}
int parser::parse_fontindex() {
string s = get_tag_value();
if (s.empty() || s == "-") {
return 0;
}
try {
size_t ptr;
int ret = std::stoi(s, &ptr, 10);
if (ret < 0) {
return 0;
}
if (ptr != s.size()) {
throw font_error(s, "Font index contains non-number characters");
}
return ret;
} catch (const std::exception& err) {
throw font_error(s, err.what());
}
}
int parser::parse_offset() {
string s = get_tag_value();
if (s.empty()) {
return 0;
}
try {
size_t ptr;
int ret = std::stoi(s, &ptr, 10);
if (ptr != s.size()) {
throw offset_error(s, "Offset contains non-number characters");
}
return ret;
} catch (const std::exception& err) {
throw offset_error(s, err.what());
}
return 0;
}
controltag parser::parse_control() {
string s = get_tag_value();
if (s.empty()) {
throw control_error(s, "Control tag is empty");
}
switch (s[0]) {
case 'R':
if (s.size() != 1) {
throw control_error(s, "Control tag R has extra data");
}
return controltag::R;
default:
throw control_error(s);
}
}
std::pair<action_value, string> parser::parse_action() {
mousebtn btn = parse_action_btn();
action_value ret;
string cmd;
if (has_next() && peek() == ':') {
ret.btn = btn == mousebtn::NONE ? mousebtn::LEFT : btn;
ret.closing = false;
cmd = parse_action_cmd();
} else {
ret.btn = btn;
ret.closing = true;
}
return {ret, cmd};
}
/**
* Parses the button index after starting an action tag.
*
* May return mousebtn::NONE if no button was given.
*/
mousebtn parser::parse_action_btn() {
if (has_next()) {
if (isdigit(peek())) {
char c = next();
int num = c - '0';
if (num < static_cast<int>(mousebtn::NONE) || num >= static_cast<int>(mousebtn::BTN_COUNT)) {
throw btn_error(string{c});
}
return static_cast<mousebtn>(num);
}
}
return mousebtn::NONE;
}
/**
* Starts at ':' and parses a complete action string.
*
* Returns the parsed action string with without escaping backslashes.
*
* Afterwards the parsers will be at the character immediately after the
* closing colon.
*/
string parser::parse_action_cmd() {
consume(':');
string s;
char prev = EOL;
while (has_next()) {
char c = next();
if (c == ':') {
if (prev == '\\') {
s.pop_back();
s.push_back(c);
} else {
break;
}
} else {
s.push_back(c);
}
prev = c;
}
return s;
}
attribute parser::parse_attribute() {
char c;
switch (c = next()) {
case 'u':
return attribute::UNDERLINE;
case 'o':
return attribute::OVERLINE;
default:
throw unrecognized_attr(c);
}
}
void parser::push_char(char c) {
if (!buf.empty() && buf_pos < buf.size() && !buf.back().is_tag) {
buf.back().data += c;
} else {
buf.emplace_back(string{c});
}
}
void parser::push_text(string&& text) {
if (text.empty()) {
return;
}
if (!buf.empty() && buf_pos < buf.size() && !buf.back().is_tag) {
buf.back().data += text;
} else {
buf.emplace_back(std::move(text));
}
}
/**
* Will read up until the end of the tag value.
*
* Afterwards the parser will be at the character directly after the tag
* value.
*
* This function just reads until it encounters a space or a closing curly
* bracket, so it is not useful for tag values that can contain these
* characters (e.g. action tags).
*/
string parser::get_tag_value() {
string s;
while (has_next() && peek() != ' ' && peek() != '}') {
s.push_back(next());
}
return s;
}
} // namespace tags
POLYBAR_NS_END

View File

@ -1,5 +1,7 @@
#include "utils/color.hpp"
#include <algorithm>
POLYBAR_NS
/**
@ -24,6 +26,11 @@ static string normalize_hex(string hex) {
hex.erase(0, 1);
}
// Check that only valid characters are used
if (!std::all_of(hex.cbegin(), hex.cend(), isxdigit)) {
return "";
}
if (hex.length() == 2) {
// We only have an alpha channel
return hex;
@ -51,20 +58,20 @@ static string normalize_hex(string hex) {
return hex;
}
rgba::rgba() : m_value(0), m_type(NONE) {}
rgba::rgba(uint32_t value, color_type type) : m_value(value), m_type(type) {}
rgba::rgba() : m_value(0), m_type(type::NONE) {}
rgba::rgba(uint32_t value, enum type type) : m_value(value), m_type(type) {}
rgba::rgba(string hex) {
hex = normalize_hex(hex);
if (hex.length() == 0) {
m_value = 0;
m_type = NONE;
m_type = type::NONE;
} else if (hex.length() == 2) {
m_value = std::strtoul(hex.c_str(), nullptr, 16) << 24;
m_type = ALPHA_ONLY;
m_type = type::ALPHA_ONLY;
} else {
m_value = std::strtoul(hex.c_str(), nullptr, 16);
m_type = ARGB;
m_type = type::ARGB;
}
}
@ -80,11 +87,11 @@ bool rgba::operator==(const rgba& other) const {
}
switch (m_type) {
case NONE:
case type::NONE:
return true;
case ARGB:
case type::ARGB:
return m_value == other.m_value;
case ALPHA_ONLY:
case type::ALPHA_ONLY:
return alpha_i() == other.alpha_i();
default:
return false;
@ -99,7 +106,7 @@ uint32_t rgba::value() const {
return this->m_value;
}
rgba::color_type rgba::type() const {
enum rgba::type rgba::type() const {
return m_type;
}
@ -136,7 +143,7 @@ uint8_t rgba::blue_i() const {
}
bool rgba::has_color() const {
return m_type != NONE;
return m_type != type::NONE;
}
/**
@ -154,7 +161,7 @@ rgba rgba::apply_alpha_to(rgba other) const {
* \returns the new color if this is ALPHA_ONLY or a copy of this otherwise.
*/
rgba rgba::try_apply_alpha_to(rgba other) const {
if (m_type == ALPHA_ONLY) {
if (m_type == type::ALPHA_ONLY) {
return apply_alpha_to(other);
}

View File

@ -50,19 +50,19 @@ endfunction()
add_unit_test(utils/actions)
add_unit_test(utils/color)
add_unit_test(utils/command)
add_unit_test(utils/math unit_tests)
add_unit_test(utils/memory unit_tests)
add_unit_test(utils/scope unit_tests)
add_unit_test(utils/string unit_tests)
add_unit_test(utils/math)
add_unit_test(utils/memory)
add_unit_test(utils/scope)
add_unit_test(utils/string)
add_unit_test(utils/file)
add_unit_test(utils/process)
add_unit_test(components/command_line)
add_unit_test(components/bar)
add_unit_test(components/parser)
add_unit_test(components/config_parser)
add_unit_test(drawtypes/label)
add_unit_test(drawtypes/ramp)
add_unit_test(drawtypes/iconset)
add_unit_test(tags/parser)
# Run make check to build and run all unit tests
add_custom_target(check

View File

@ -1,37 +0,0 @@
#include "common/test.hpp"
#include "events/signal_emitter.hpp"
#include "components/parser.hpp"
using namespace polybar;
class TestableParser : public parser {
using parser::parser;
public: using parser::parse_action_cmd;
};
class Parser : public ::testing::Test {
protected:
TestableParser m_parser{signal_emitter::make()};
};
/**
* The first element of the pair is the expected return text, the second element
* is the input to parse_action_cmd
*/
class ParseActionCmd :
public Parser,
public ::testing::WithParamInterface<pair<string, string>> {};
vector<pair<string, string>> parse_action_cmd_list = {
{"abc", ":abc:\\abc"},
{"abc\\:", ":abc\\::\\abc"},
{"\\:\\:\\:", ":\\:\\:\\::\\abc"},
};
INSTANTIATE_TEST_SUITE_P(Inst, ParseActionCmd,
::testing::ValuesIn(parse_action_cmd_list));
TEST_P(ParseActionCmd, correctness) {
auto input = GetParam().second;
auto result = m_parser.parse_action_cmd(std::move(input));
EXPECT_EQ(GetParam().first, result);
}

View File

@ -0,0 +1,460 @@
#include "tags/parser.hpp"
#include "common/test.hpp"
using namespace polybar;
using namespace tags;
/**
* Helper class to test parsed data.
*
* The expect_* functions will check that the current element corresponds to
* what is expected and then move to the next element.
*
* The assert_* functions are used internally to check certain properties of the
* current element.
*/
class TestableTagParser : public parser {
public:
TestableTagParser() : TestableTagParser(""){};
TestableTagParser(const string&& input) {
setup_parser_test(std::move(input));
}
void setup_parser_test(const string& input) {
this->set(std::move(input));
}
void expect_done() {
EXPECT_FALSE(has_next_element());
}
void expect_text(const string&& exp) {
set_current();
assert_is_tag(false);
EXPECT_EQ(exp, current.data);
}
void expect_color_reset(syntaxtag t) {
set_current();
assert_format(t);
EXPECT_EQ(color_type::RESET, current.tag_data.color.type);
}
void expect_color(syntaxtag t, const string& color) {
set_current();
assert_format(t);
rgba c{color};
EXPECT_EQ(color_type::COLOR, current.tag_data.color.type);
EXPECT_EQ(c, current.tag_data.color.val);
}
void expect_action_closing(mousebtn exp = mousebtn::NONE) {
set_current();
assert_format(syntaxtag::A);
EXPECT_TRUE(current.tag_data.action.closing);
EXPECT_EQ(exp, current.tag_data.action.btn);
}
void expect_action(const string& exp, mousebtn btn) {
set_current();
assert_format(syntaxtag::A);
EXPECT_FALSE(current.tag_data.action.closing);
EXPECT_EQ(btn, current.tag_data.action.btn);
EXPECT_EQ(exp, current.data);
}
void expect_font_reset() {
expect_font(0);
}
void expect_font(unsigned exp) {
set_current();
assert_format(syntaxtag::T);
EXPECT_EQ(exp, current.tag_data.font);
}
void expect_offset(int exp) {
set_current();
assert_format(syntaxtag::O);
EXPECT_EQ(exp, current.tag_data.offset);
}
void expect_ctrl(controltag exp) {
set_current();
assert_format(syntaxtag::P);
EXPECT_EQ(exp, current.tag_data.ctrl);
}
void expect_alignment(syntaxtag exp) {
set_current();
assert_format(exp);
}
void expect_activation(attr_activation act, attribute attr) {
set_current();
assert_type(tag_type::ATTR);
EXPECT_EQ(act, current.tag_data.subtype.activation);
EXPECT_EQ(attr, current.tag_data.attr);
}
void expect_reverse() {
set_current();
assert_format(syntaxtag::R);
}
private:
void assert_format(syntaxtag exp) {
assert_type(tag_type::FORMAT);
ASSERT_EQ(exp, current.tag_data.subtype.format);
}
void assert_type(tag_type exp) {
assert_is_tag(true);
ASSERT_EQ(exp, current.tag_data.type);
}
void assert_is_tag(bool exp) {
ASSERT_EQ(exp, current.is_tag);
}
void assert_has() {
if (!has_next_element()) {
throw std::runtime_error("no next element");
}
}
void set_current() {
assert_has();
current = next_element();
}
element current;
};
class TagParserTest : public ::testing::Test {
protected:
TestableTagParser p;
};
TEST_F(TagParserTest, empty) {
p.setup_parser_test("");
p.expect_done();
}
TEST_F(TagParserTest, text) {
p.setup_parser_test("text");
p.expect_text("text");
p.expect_done();
}
// Single Tag {{{
// Parse Single Color {{{
/**
* <input, tag, colorstring>
*
* If the color string is empty, this is supposed to be a color reset
*/
using single_color = std::tuple<string, syntaxtag, string>;
class ParseSingleColorTest : public TagParserTest, public ::testing::WithParamInterface<single_color> {};
vector<single_color> parse_single_color_list = {
{"%{B-}", syntaxtag::B, ""},
{"%{F-}", syntaxtag::F, ""},
{"%{o-}", syntaxtag::o, ""},
{"%{u-}", syntaxtag::u, ""},
{"%{B}", syntaxtag::B, ""},
{"%{F}", syntaxtag::F, ""},
{"%{o}", syntaxtag::o, ""},
{"%{u}", syntaxtag::u, ""},
{"%{B#f0f0f0}", syntaxtag::B, "#f0f0f0"},
{"%{F#abc}", syntaxtag::F, "#abc"},
{"%{o#abcd}", syntaxtag::o, "#abcd"},
{"%{u#FDE}", syntaxtag::u, "#FDE"},
{"%{ u#FDE}", syntaxtag::u, "#FDE"},
};
INSTANTIATE_TEST_SUITE_P(Inst, ParseSingleColorTest, ::testing::ValuesIn(parse_single_color_list));
TEST_P(ParseSingleColorTest, correctness) {
string input;
syntaxtag t;
string color;
std::tie(input, t, color) = GetParam();
p.setup_parser_test(std::move(input));
if (color.empty()) {
p.expect_color_reset(t);
} else {
p.expect_color(t, color);
}
p.expect_done();
}
// }}}
// Parse Single Action {{{
/**
* If the third element is an empty string, this is a closing tag.
*/
using single_action = std::tuple<string, mousebtn, string>;
class ParseSingleActionTest : public TagParserTest, public ::testing::WithParamInterface<single_action> {};
vector<single_action> parse_single_action_list = {
{"%{A:cmd:}", mousebtn::LEFT, "cmd"},
{"%{A1:cmd:}", mousebtn::LEFT, "cmd"},
{"%{A2:cmd:}", mousebtn::MIDDLE, "cmd"},
{"%{A3:cmd:}", mousebtn::RIGHT, "cmd"},
{"%{A4:cmd:}", mousebtn::SCROLL_UP, "cmd"},
{"%{A5:cmd:}", mousebtn::SCROLL_DOWN, "cmd"},
{"%{A6:cmd:}", mousebtn::DOUBLE_LEFT, "cmd"},
{"%{A7:cmd:}", mousebtn::DOUBLE_MIDDLE, "cmd"},
{"%{A8:cmd:}", mousebtn::DOUBLE_RIGHT, "cmd"},
{"%{A}", mousebtn::NONE, ""},
{"%{A1}", mousebtn::LEFT, ""},
{"%{A2}", mousebtn::MIDDLE, ""},
{"%{A3}", mousebtn::RIGHT, ""},
{"%{A4}", mousebtn::SCROLL_UP, ""},
{"%{A5}", mousebtn::SCROLL_DOWN, ""},
{"%{A6}", mousebtn::DOUBLE_LEFT, ""},
{"%{A7}", mousebtn::DOUBLE_MIDDLE, ""},
{"%{A8}", mousebtn::DOUBLE_RIGHT, ""},
{"%{A1:a\\:b:}", mousebtn::LEFT, "a:b"},
{"%{A1:\\:\\:\\::}", mousebtn::LEFT, ":::"},
{"%{A1:#apps.open.0:}", mousebtn::LEFT, "#apps.open.0"},
// https://github.com/polybar/polybar/issues/2040
{"%{A1:cmd | awk '{ print $NF }'):}", mousebtn::LEFT, "cmd | awk '{ print $NF }')"},
};
INSTANTIATE_TEST_SUITE_P(Inst, ParseSingleActionTest, ::testing::ValuesIn(parse_single_action_list));
TEST_P(ParseSingleActionTest, correctness) {
string input;
mousebtn btn;
string cmd;
std::tie(input, btn, cmd) = GetParam();
p.setup_parser_test(std::move(input));
if (cmd.empty()) {
p.expect_action_closing(btn);
} else {
p.expect_action(cmd, btn);
}
p.expect_done();
}
// }}}
// Parse Single Activation {{{
using single_activation = std::tuple<string, attribute, attr_activation>;
class ParseSingleActivationTest : public TagParserTest, public ::testing::WithParamInterface<single_activation> {};
vector<single_activation> parse_single_activation_list = {
{"%{+u}", attribute::UNDERLINE, attr_activation::ON},
{"%{-u}", attribute::UNDERLINE, attr_activation::OFF},
{"%{!u}", attribute::UNDERLINE, attr_activation::TOGGLE},
{"%{+o}", attribute::OVERLINE, attr_activation::ON},
{"%{-o}", attribute::OVERLINE, attr_activation::OFF},
{"%{!o}", attribute::OVERLINE, attr_activation::TOGGLE},
};
INSTANTIATE_TEST_SUITE_P(Inst, ParseSingleActivationTest, ::testing::ValuesIn(parse_single_activation_list));
TEST_P(ParseSingleActivationTest, correctness) {
string input;
attribute attr;
attr_activation act;
std::tie(input, attr, act) = GetParam();
p.setup_parser_test(std::move(input));
p.expect_activation(act, attr);
p.expect_done();
}
// }}}
TEST_F(TagParserTest, reverse) {
p.setup_parser_test("%{R}");
p.expect_reverse();
p.expect_done();
}
TEST_F(TagParserTest, font) {
p.setup_parser_test("%{T}");
p.expect_font_reset();
p.expect_done();
p.setup_parser_test("%{T-}");
p.expect_font_reset();
p.expect_done();
p.setup_parser_test("%{T-123}");
p.expect_font_reset();
p.expect_done();
p.setup_parser_test("%{T123}");
p.expect_font(123);
p.expect_done();
}
TEST_F(TagParserTest, offset) {
p.setup_parser_test("%{O}");
p.expect_offset(0);
p.expect_done();
p.setup_parser_test("%{O0}");
p.expect_offset(0);
p.expect_done();
p.setup_parser_test("%{O-112}");
p.expect_offset(-112);
p.expect_done();
p.setup_parser_test("%{O123}");
p.expect_offset(123);
p.expect_done();
}
TEST_F(TagParserTest, alignment) {
p.setup_parser_test("%{l}");
p.expect_alignment(syntaxtag::l);
p.expect_done();
p.setup_parser_test("%{c}");
p.expect_alignment(syntaxtag::c);
p.expect_done();
p.setup_parser_test("%{r}");
p.expect_alignment(syntaxtag::r);
p.expect_done();
}
TEST_F(TagParserTest, ctrl) {
p.setup_parser_test("%{PR}");
p.expect_ctrl(controltag::R);
p.expect_done();
}
/**
* Tests the the legacy %{U...} tag first produces %{u...} and then %{o...}
*/
TEST_F(TagParserTest, UnderOverLine) {
p.setup_parser_test("%{U-}");
p.expect_color_reset(syntaxtag::u);
p.expect_color_reset(syntaxtag::o);
p.expect_done();
p.setup_parser_test("%{U#12ab}");
p.expect_color(syntaxtag::u, "#12ab");
p.expect_color(syntaxtag::o, "#12ab");
p.expect_done();
}
// }}}
TEST_F(TagParserTest, compoundTags) {
p.setup_parser_test("%{F- B#ff0000 A:cmd:}");
p.expect_color_reset(syntaxtag::F);
p.expect_color(syntaxtag::B, "#ff0000");
p.expect_action("cmd", mousebtn::LEFT);
p.expect_done();
}
TEST_F(TagParserTest, combinations) {
p.setup_parser_test("%{r}%{u#4bffdc +u u#4bffdc} 20% abc%{-u u- PR}");
p.expect_alignment(syntaxtag::r);
p.expect_color(syntaxtag::u, "#4bffdc");
p.expect_activation(attr_activation::ON, attribute::UNDERLINE);
p.expect_color(syntaxtag::u, "#4bffdc");
p.expect_text(" 20% abc");
p.expect_activation(attr_activation::OFF, attribute::UNDERLINE);
p.expect_color_reset(syntaxtag::u);
p.expect_ctrl(controltag::R);
p.expect_done();
}
/**
* The type of exception we expect.
*
* Since we can't directly pass typenames, we go through this enum.
*/
enum class exc { ERR, TOKEN, TAG, TAG_END, COLOR, ATTR, FONT, CTRL, OFFSET, BTN };
using exception_test = pair<string, enum exc>;
class ParseErrorTest : public TagParserTest, public ::testing::WithParamInterface<exception_test> {};
vector<exception_test> parse_error_test = {
{"%{F-", exc::TAG_END},
{"%{Q", exc::TAG},
{"%{", exc::TOKEN},
{"%{F#xyz}", exc::COLOR},
{"%{Ffoo}", exc::COLOR},
{"%{F-abc}", exc::COLOR},
{"%{+z}", exc::ATTR},
{"%{T-abc}", exc::FONT},
{"%{T12a}", exc::FONT},
{"%{Tabc}", exc::FONT},
{"%{?u", exc::TAG},
{"%{PRabc}", exc::CTRL},
{"%{P}", exc::CTRL},
{"%{PA}", exc::CTRL},
{"%{Oabc}", exc::OFFSET},
{"%{A2:cmd:cmd:}", exc::TAG_END},
{"%{A9}", exc::BTN},
{"%{rQ}", exc::TAG_END},
};
INSTANTIATE_TEST_SUITE_P(Inst, ParseErrorTest, ::testing::ValuesIn(parse_error_test));
TEST_P(ParseErrorTest, correctness) {
string input;
exc exception;
std::tie(input, exception) = GetParam();
p.setup_parser_test(input);
ASSERT_TRUE(p.has_next_element());
switch (exception) {
case exc::ERR:
ASSERT_THROW(p.next_element(), tags::error);
break;
case exc::TOKEN:
ASSERT_THROW(p.next_element(), tags::token_error);
break;
case exc::TAG:
ASSERT_THROW(p.next_element(), tags::unrecognized_tag);
break;
case exc::TAG_END:
ASSERT_THROW(p.next_element(), tags::tag_end_error);
break;
case exc::COLOR:
ASSERT_THROW(p.next_element(), tags::color_error);
break;
case exc::ATTR:
ASSERT_THROW(p.next_element(), tags::unrecognized_attr);
break;
case exc::FONT:
ASSERT_THROW(p.next_element(), tags::font_error);
break;
case exc::CTRL:
ASSERT_THROW(p.next_element(), tags::control_error);
break;
case exc::OFFSET:
ASSERT_THROW(p.next_element(), tags::offset_error);
break;
case exc::BTN:
ASSERT_THROW(p.next_element(), tags::btn_error);
break;
default:
FAIL();
}
}

View File

@ -9,7 +9,10 @@ TEST(Rgba, constructor) {
EXPECT_FALSE(rgba("#f").has_color());
EXPECT_EQ(rgba::ALPHA_ONLY, rgba{"#12"}.type());
EXPECT_FALSE(rgba("#-abc").has_color());
EXPECT_FALSE(rgba("#xyz").has_color());
EXPECT_EQ(rgba::type::ALPHA_ONLY, rgba{"#12"}.type());
EXPECT_EQ(0xff000000, rgba{"#ff"}.value());
@ -49,22 +52,22 @@ TEST(Rgba, string) {
}
TEST(Rgba, eq) {
rgba v(0x12, rgba::NONE);
rgba v(0x12, rgba::type::NONE);
EXPECT_TRUE(v == rgba(0, rgba::NONE));
EXPECT_TRUE(v == rgba(0x11, rgba::NONE));
EXPECT_TRUE(v == rgba(0, rgba::type::NONE));
EXPECT_TRUE(v == rgba(0x11, rgba::type::NONE));
EXPECT_FALSE(v == rgba{0x123456});
v = rgba{0xCC123456};
EXPECT_TRUE(v == rgba{0xCC123456});
EXPECT_FALSE(v == rgba(0xCC123456, rgba::NONE));
EXPECT_FALSE(v == rgba(0xCC123456, rgba::type::NONE));
v = rgba{"#aa"};
EXPECT_TRUE(v == rgba(0xaa000000, rgba::ALPHA_ONLY));
EXPECT_FALSE(v == rgba(0xaa000000, rgba::ARGB));
EXPECT_FALSE(v == rgba(0xab000000, rgba::ALPHA_ONLY));
EXPECT_TRUE(v == rgba(0xaa000000, rgba::type::ALPHA_ONLY));
EXPECT_FALSE(v == rgba(0xaa000000, rgba::type::ARGB));
EXPECT_FALSE(v == rgba(0xab000000, rgba::type::ALPHA_ONLY));
}
TEST(Rgba, hasColor) {
@ -80,7 +83,7 @@ TEST(Rgba, hasColor) {
EXPECT_TRUE(v.has_color());
v = rgba(0x1243, rgba::NONE);
v = rgba(0x1243, rgba::type::NONE);
EXPECT_FALSE(v.has_color());
}
@ -97,8 +100,8 @@ TEST(Rgba, channel) {
}
TEST(Rgba, applyAlphaTo) {
rgba v{0xAA000000, rgba::ALPHA_ONLY};
rgba modified = v.apply_alpha_to(rgba{0xCC123456, rgba::ALPHA_ONLY});
rgba v{0xAA000000, rgba::type::ALPHA_ONLY};
rgba modified = v.apply_alpha_to(rgba{0xCC123456});
EXPECT_EQ(0xAA123456, modified.value());
v = rgba{0xCC999999};
@ -107,8 +110,8 @@ TEST(Rgba, applyAlphaTo) {
}
TEST(Rgba, tryApplyAlphaTo) {
rgba v{0xAA000000, rgba::ALPHA_ONLY};
rgba modified = v.try_apply_alpha_to(rgba{0xCC123456, rgba::ALPHA_ONLY});
rgba v{0xAA000000, rgba::type::ALPHA_ONLY};
rgba modified = v.try_apply_alpha_to(rgba{0xCC123456});
EXPECT_EQ(0xAA123456, modified.value());
v = rgba{0xCC999999};