parser: allow string literals and nested func calls

fixes #106
This commit is contained in:
Tony Crisci
2019-06-21 15:52:09 -04:00
parent 3a647d06ec
commit eb947aa934
3 changed files with 220 additions and 168 deletions

View File

@@ -1,2 +1,3 @@
/build /build
__pycache__ __pycache__
.pytest_cache

View File

@@ -10,8 +10,8 @@
G_DEFINE_QUARK(playerctl-formatter-error-quark, playerctl_formatter_error); G_DEFINE_QUARK(playerctl-formatter-error-quark, playerctl_formatter_error);
enum token_type { enum token_type {
TOKEN_PASSTHROUGH,
TOKEN_VARIABLE, TOKEN_VARIABLE,
TOKEN_STRING,
TOKEN_FUNCTION, TOKEN_FUNCTION,
}; };
@@ -22,10 +22,9 @@ struct token {
}; };
enum parser_state { enum parser_state {
STATE_INSIDE = 0, STATE_EXPRESSION = 0,
STATE_PARAMS_OPEN, STATE_IDENTIFIER,
STATE_PARAMS_CLOSED, STATE_STRING,
STATE_PASSTHROUGH,
}; };
struct _PlayerctlFormatterPrivate { struct _PlayerctlFormatterPrivate {
@@ -79,7 +78,109 @@ static gboolean token_list_contains_key(GList *tokens, const gchar *key) {
return FALSE; return FALSE;
} }
static gboolean is_identifier_char(gchar c) {
return g_ascii_isalnum(c) || c == '_' || c == ':' || c == '-';
}
static struct token *tokenize_expression(const gchar *format, gint pos, gint *end, GError **error) {
GError *tmp_error = NULL;
int len = strlen(format);
char buf[1028];
int buf_len = 0;
enum parser_state state = STATE_EXPRESSION;
for (int i = pos; i < len; ++i) {
switch (state) {
case STATE_EXPRESSION:
if (format[i] == ' ') {
continue;
} else if (format[i] == '"') {
state = STATE_STRING;
continue;
} else if (!is_identifier_char(format[i])) {
// TODO return NULL to indicate there is no expression
g_set_error(error, playerctl_formatter_error_quark(), 1,
"unexpected \"%c\", expected expression (position %d)", format[i], i);
return NULL;
} else {
state = STATE_IDENTIFIER;
buf[buf_len++] = format[i];
continue;
}
break;
case STATE_STRING:
if (format[i] == '"') {
struct token *ret = token_create(TOKEN_STRING);
buf[buf_len] = '\0';
ret->data = g_strdup(buf);
i++;
while (i < len && format[i] == ' ') {
i++;
}
*end = i;
//printf("string: '%s'\n", ret->data);
return ret;
} else {
buf[buf_len++] = format[i];
}
break;
case STATE_IDENTIFIER:
if (format[i] == '(') {
struct token *ret = token_create(TOKEN_FUNCTION);
buf[buf_len] = '\0';
ret->data = g_strdup(buf);
i += 1;
//printf("function: '%s'\n", ret->data);
ret->arg = tokenize_expression(format, i, end, &tmp_error);
while (*end < len && format[*end] == ' ') {
*end += 1;
}
if (tmp_error != NULL) {
token_destroy(ret);
g_propagate_error(error, tmp_error);
return NULL;
}
if (format[*end] != ')') {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"expecting \")\" (position %d)", *end);
}
*end += 1;
return ret;
} else if (!is_identifier_char(format[i])) {
struct token *ret = token_create(TOKEN_VARIABLE);
buf[buf_len] = '\0';
ret->data = g_strdup(buf);
while (i < len && format[i] == ' ') {
i++;
}
*end = i;
//printf("variable: '%s' end='%c'\n", ret->data, format[*end]);
return ret;
} else {
buf[buf_len] = format[i];
++buf_len;
}
break;
}
}
g_set_error(error, playerctl_formatter_error_quark(), 1,
"unexpected end of format string");
return NULL;
}
static GList *tokenize_format(const char *format, GError **error) { static GList *tokenize_format(const char *format, GError **error) {
GError *tmp_error = NULL;
GList *tokens = NULL; GList *tokens = NULL;
if (format == NULL) { if (format == NULL) {
@@ -96,133 +197,50 @@ static GList *tokenize_format(const char *format, GError **error) {
return NULL; return NULL;
} }
enum parser_state state = STATE_PASSTHROUGH;
for (int i = 0; i < len; ++i) { for (int i = 0; i < len; ++i) {
if (format[i] == '{' && i < len + 1 && format[i+1] == '{') { if (format[i] == '{' && i < len + 1 && format[i+1] == '{') {
if (state == STATE_INSIDE) { if (buf_len > 0) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"unexpected token: \"{{\" (position %d)", i);
token_list_destroy(tokens);
return NULL;
}
if (buf_len != 0) {
struct token *token = token_create(TOKEN_PASSTHROUGH);
buf[buf_len] = '\0'; buf[buf_len] = '\0';
buf_len = 0;
struct token *token = token_create(TOKEN_STRING);
token->data = g_strdup(buf); token->data = g_strdup(buf);
//printf("passthrough: '%s'\n", token->data);
tokens = g_list_append(tokens, token); tokens = g_list_append(tokens, token);
} }
i += 1;
buf_len = 0;
state = STATE_INSIDE;
} else if (format[i] == '}' && i < len + 1 && format[i+1] == '}' && state != STATE_PASSTHROUGH) {
if (state == STATE_PARAMS_OPEN) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"unexpected token: \"}}\" (expected closing parens: \")\" at position %d)", i);
token_list_destroy(tokens);
return NULL;
}
if (state != STATE_PARAMS_CLOSED) { i += 2;
buf[buf_len] = '\0'; int end = 0;
gchar *name = g_strstrip(g_strdup(buf)); struct token *token = tokenize_expression(format, i, &end, &tmp_error);
if (strlen(name) == 0) { if (tmp_error != NULL) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"got empty template expression at position %d", i);
token_list_destroy(tokens);
g_free(name);
return NULL;
}
struct token *token = token_create(TOKEN_VARIABLE);
token->data = name;
tokens = g_list_append(tokens, token);
} else if (buf_len > 0) {
for (int k = 0; k < buf_len; ++k) {
if (buf[k] != ' ') {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"got unexpected input after closing parens at position %d", i - buf_len + k);
token_list_destroy(tokens);
return NULL;
}
}
}
i += 1;
buf_len = 0;
state = STATE_PASSTHROUGH;
} else if (format[i] == '(' && state != STATE_PASSTHROUGH) {
if (state == STATE_PARAMS_OPEN) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"unexpected token: \"(\" at position %d", i);
token_list_destroy(tokens); token_list_destroy(tokens);
g_propagate_error(error, tmp_error);
return NULL; return NULL;
} }
if (state == STATE_PARAMS_CLOSED) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"unexpected token: \"(\" at position %d", i);
token_list_destroy(tokens);
return NULL;
}
buf[buf_len] = '\0';
gchar *name = g_strstrip(g_strdup(buf));
if (strlen(name) == 0) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"expected a function name to call at position %d", i);
token_list_destroy(tokens);
g_free(name);
return NULL;
}
struct token *token = token_create(TOKEN_FUNCTION);
token->data = name;
tokens = g_list_append(tokens, token); tokens = g_list_append(tokens, token);
buf_len = 0; i = end;
state = STATE_PARAMS_OPEN;
} else if (format[i] == ')' && state != STATE_PASSTHROUGH) {
if (state != STATE_PARAMS_OPEN) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"unexpected token: \")\" at position %d", i);
token_list_destroy(tokens);
return NULL;
}
buf[buf_len] = '\0';
gchar *name = g_strstrip(g_strdup(buf));
if (strlen(name) == 0) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"expected a function parameter at position %d", i);
token_list_destroy(tokens);
g_free(name);
return NULL;
}
struct token *token = token_create(TOKEN_VARIABLE);
token->data = name;
struct token *fn_token = g_list_last(tokens)->data; while (i < len && format[i] == ' ') {
assert(fn_token != NULL); i++;
assert(fn_token->type == TOKEN_FUNCTION); }
assert(fn_token->arg == NULL);
fn_token->arg = token; if (i >= len || format[i] != '}' || format[i+1] != '}') {
buf_len = 0; token_list_destroy(tokens);
state = STATE_PARAMS_CLOSED; g_set_error(error, playerctl_formatter_error_quark(), 1,
"expecting \"}}\" (position %d)", i);
return NULL;
}
i += 1;
} else if (format[i] == '}' && i < len + 1 && format[i+1] == '}') {
assert(FALSE && "TODO");
} else { } else {
buf[buf_len++] = format[i]; buf[buf_len++] = format[i];
} }
} }
if (state == STATE_INSIDE || state == STATE_PARAMS_CLOSED) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"unmatched opener \"{{\" (expected a matching \"}}\" at the end)");
token_list_destroy(tokens);
return NULL;
} else if (state == STATE_PARAMS_OPEN) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"unmatched opener \"(\" (expected a matching \")\")");
token_list_destroy(tokens);
return NULL;
}
if (buf_len > 0) { if (buf_len > 0) {
buf[buf_len] = '\0'; buf[buf_len] = '\0';
struct token *token = token_create(TOKEN_PASSTHROUGH); struct token *token = token_create(TOKEN_STRING);
token->data = g_strdup(buf); token->data = g_strdup(buf);
tokens = g_list_append(tokens, token); tokens = g_list_append(tokens, token);
} }
@@ -238,6 +256,10 @@ static gchar *helperfn_lc(gchar *key, GVariant *value) {
} }
static gchar *helperfn_uc(gchar *key, GVariant *value) { static gchar *helperfn_uc(gchar *key, GVariant *value) {
if (value == NULL) {
return g_strdup("");
}
gchar *printed = pctl_print_gvariant(value); gchar *printed = pctl_print_gvariant(value);
gchar *printed_uc = g_utf8_strup(printed, -1); gchar *printed_uc = g_utf8_strup(printed, -1);
g_free(printed); g_free(printed);
@@ -245,6 +267,10 @@ static gchar *helperfn_uc(gchar *key, GVariant *value) {
} }
static gchar *helperfn_duration(gchar *key, GVariant *value) { static gchar *helperfn_duration(gchar *key, GVariant *value) {
if (value == NULL) {
return g_strdup("");
}
// mpris durations are represented as int64 in microseconds // mpris durations are represented as int64 in microseconds
if (!g_variant_type_equal(g_variant_get_type(value), G_VARIANT_TYPE_INT64)) { if (!g_variant_type_equal(g_variant_get_type(value), G_VARIANT_TYPE_INT64)) {
return NULL; return NULL;
@@ -269,6 +295,10 @@ static gchar *helperfn_duration(gchar *key, GVariant *value) {
/* Calls g_markup_escape_text to replace the text with appropriately escaped /* Calls g_markup_escape_text to replace the text with appropriately escaped
characters for XML */ characters for XML */
static gchar *helperfn_markup_escape(gchar *key, GVariant *value) { static gchar *helperfn_markup_escape(gchar *key, GVariant *value) {
if (value == NULL) {
return g_strdup("");
}
gchar *printed = pctl_print_gvariant(value); gchar *printed = pctl_print_gvariant(value);
gchar *escaped = g_markup_escape_text(printed, -1); gchar *escaped = g_markup_escape_text(printed, -1);
g_free(printed); g_free(printed);
@@ -277,6 +307,10 @@ static gchar *helperfn_markup_escape(gchar *key, GVariant *value) {
static gchar *helperfn_emoji(gchar *key, GVariant *value) { static gchar *helperfn_emoji(gchar *key, GVariant *value) {
g_warning("The emoji() helper function is undocumented and experimental and will change in a future release."); g_warning("The emoji() helper function is undocumented and experimental and will change in a future release.");
if (value == NULL) {
return g_strdup("");
}
if (g_strcmp0(key, "status") == 0 && if (g_strcmp0(key, "status") == 0 &&
g_variant_is_of_type(value, G_VARIANT_TYPE_STRING)) { g_variant_is_of_type(value, G_VARIANT_TYPE_STRING)) {
const gchar *status_str = g_variant_get_string(value, NULL); const gchar *status_str = g_variant_get_string(value, NULL);
@@ -318,73 +352,78 @@ struct template_helper {
{"emoji", &helperfn_emoji}, {"emoji", &helperfn_emoji},
}; };
static gchar *expand_format(GList *tokens, GVariantDict *context, GError **error) { static GVariant *expand_token(struct token *token, GVariantDict *context, GError **error) {
GString *expanded; GError *tmp_error = NULL;
expanded = g_string_new(""); switch (token->type) {
GList *next = tokens; case TOKEN_STRING:
while (next != NULL) { return g_variant_new("s", token->data);
struct token *token = next->data;
switch (token->type) { case TOKEN_VARIABLE:
case TOKEN_PASSTHROUGH: if (g_variant_dict_contains(context, token->data)) {
expanded = g_string_append(expanded, token->data); return g_variant_dict_lookup_value(context, token->data, NULL);
break; } else {
case TOKEN_VARIABLE: return NULL;
{
gchar *name = token->data;
if (g_variant_dict_contains(context, name)) {
GVariant *value = g_variant_dict_lookup_value(context, name, NULL);
if (value != NULL) {
gchar *value_str = pctl_print_gvariant(value);
expanded = g_string_append(expanded, value_str);
g_variant_unref(value);
g_free(value_str);
}
}
break;
} }
case TOKEN_FUNCTION:
case TOKEN_FUNCTION:
{ {
// XXX: functions must have an argument and that argument must be a
// variable (enforced in the tokenization step)
assert(token->arg != NULL); assert(token->arg != NULL);
assert(token->arg->type == TOKEN_VARIABLE); gchar *arg_name = NULL;
if (token->type == TOKEN_VARIABLE) {
gboolean found = FALSE; arg_name = token->data;
gchar *fn_name = token->data;
gchar *arg_name = token->arg->data;
for (gsize i = 0; i < LENGTH(helpers); ++i) {
if (g_strcmp0(helpers[i].name, fn_name) == 0) {
GVariant *value = g_variant_dict_lookup_value(context, arg_name, NULL);
if (value != NULL) {
gchar *result = helpers[i].func(arg_name, value);
if (result != NULL) {
expanded = g_string_append(expanded, result);
g_free(result);
}
g_variant_unref(value);
}
found = TRUE;
break;
}
} }
if (!found) { GVariant *value = expand_token(token->arg, context, &tmp_error);
g_set_error(error, playerctl_formatter_error_quark(), 1,
"unknown template function: %s", fn_name); if (tmp_error != NULL) {
token_list_destroy(tokens); g_propagate_error(error, tmp_error);
g_string_free(expanded, TRUE);
return NULL; return NULL;
} }
break; for (gsize i = 0; i < LENGTH(helpers); ++i) {
} if (g_strcmp0(helpers[i].name, token->data) == 0) {
} gchar *result = helpers[i].func(arg_name, value);
if (value != NULL) {
g_variant_unref(value);
}
return g_variant_new("s", result);
}
}
next = next->next; if (value != NULL) {
g_variant_unref(value);
}
g_set_error(error, playerctl_formatter_error_quark(), 1,
"unknown template function: %s", token->data);
return NULL;
}
} }
assert(FALSE && "not reached");
return NULL;
}
static gchar *expand_format(GList *tokens, GVariantDict *context, GError **error) {
GError *tmp_error = NULL;
GString *expanded;
expanded = g_string_new("");
GList *t = tokens;
for (t = tokens; t != NULL; t = t->next) {
GVariant *value = expand_token(t->data, context, &tmp_error);
if (tmp_error != NULL) {
g_propagate_error(error, tmp_error);
return NULL;
}
if (value != NULL) {
gchar *result = pctl_print_gvariant(value);
expanded = g_string_append(expanded, result);
g_free(result);
g_variant_unref(value);
}
}
return g_string_free(expanded, FALSE); return g_string_free(expanded, FALSE);
} }

View File

@@ -35,3 +35,15 @@ async def test_format(bus_address):
cmd = await playerctl.run('metadata --format "{{uc(title)}}"') cmd = await playerctl.run('metadata --format "{{uc(title)}}"')
assert cmd.stdout == TITLE.upper() assert cmd.stdout == TITLE.upper()
cmd = await playerctl.run('metadata --format "{{uc(lc(title))}}"')
assert cmd.stdout == TITLE.upper()
cmd = await playerctl.run('metadata --format \'{{uc("Hi")}}\'')
assert cmd.stdout == "HI"
cmd = await playerctl.run('metadata --format "{{mpris:length}}"')
assert cmd.stdout == "100000"
cmd = await playerctl.run('metadata --format \'@{{ uc( "hi" ) }} - {{uc( lc( "HO" ) ) }} . {{lc( uc( title ) ) }}@\'')
assert cmd.stdout == '@HI - HO . a title@'