This adds a command to escape ASS tags to remove code duplication between sub/osd_libass.c, console.lua, osc.lua, stats.lua and any user script that calls mp.create_osd_overlay(). A command is used instead of scripting functions so that all clients can use this and not just use Lua and JS ones. osd_mangle_ass() also interprets osd-sym-cc and osd-ass-cc/{0,1}, but since they use invalid UTF-8 characters there is no risk of escape-ass users using them by accident, like with any OSD message. Always replacing \n with \\N in mangle_ass() even when it is not called by escape-ass doesn't seem to cause any issue, but I made it conditional anyway to avoid changing how all OSD messages are treated unnecessarily.
Unified · Split
| @@ -0,0 +1 @@ | ||
| 1 | +add the `escape-ass` command | |
| @@ -1259,6 +1259,18 @@ Input Commands that are Possibly Subject to Change · 1258 unmodified lines | ||
| 1259 | 1259 | use the ``mp.create_osd_overlay()`` helper instead of invoking this |
| 1260 | 1260 | command directly. |
| 1261 | 1261 | |
| 1262 | +``escape-ass <text>`` | |
| 1263 | + Modify ``text`` so that commands and functions that interpret ASS tags, | |
| 1264 | + such as ``osd-overlay`` and ``mp.create_osd_overlay``, will display it | |
| 1265 | + verbatim, and return it. This can only be used through the client API or | |
| 1266 | + from a script using ``mp.command_native``. | |
| 1267 | + | |
| 1268 | + .. admonition:: Example | |
| 1269 | + | |
| 1270 | + ``mp.osd_message(mp.command_native({"escape-ass", "foo {bar}"}))`` | |
| 1271 | + | |
| 1272 | + This line of Lua prints "foo \\{bar}" on the OSD. | |
| 1273 | + | |
| 1262 | 1274 | ``script-message [<arg1> [<arg2> [...]]]`` |
| 1263 | 1275 | Send a message to all clients, and pass it the following list of arguments. |
| 1264 | 1276 | What this message means, how many arguments it takes, and what the arguments |
| @@ -5582,6 +5582,19 @@ static void cmd_expand_path(void *p) · 5581 unmodified lines | ||
| 5582 | 5582 | }; |
| 5583 | 5583 | } |
| 5584 | 5584 | |
| 5585 | +static void cmd_escape_ass(void *p) | |
| 5586 | +{ | |
| 5587 | + struct mp_cmd_ctx *cmd = p; | |
| 5588 | + bstr dst = {0}; | |
| 5589 | + | |
| 5590 | + osd_mangle_ass(&dst, cmd->args[0].v.s, true); | |
| 5591 | + | |
| 5592 | + cmd->result = (mpv_node){ | |
| 5593 | + .format = MPV_FORMAT_STRING, | |
| 5594 | + .u.string = dst.len ? (char *)dst.start : talloc_strdup(NULL, ""), | |
| 5595 | + }; | |
| 5596 | +} | |
| 5597 | + | |
| 5585 | 5598 | static struct load_action get_load_action(struct MPContext *mpctx, int action_flag) |
| 5586 | 5599 | { |
| 5587 | 5600 | switch (action_flag) { |
| @@ -6679,6 +6692,8 @@ const struct mp_cmd_def mp_cmds[] = { · 1091 unmodified lines | ||
| 6679 | 6692 | .is_noisy = true }, |
| 6680 | 6693 | { "expand-path", cmd_expand_path, { {"text", OPT_STRING(v.s)} }, |
| 6681 | 6694 | .is_noisy = true }, |
| 6695 | + { "escape-ass", cmd_escape_ass, { {"text", OPT_STRING(v.s)} }, | |
| 6696 | + .is_noisy = true }, | |
| 6682 | 6697 | { "show-progress", cmd_show_progress, .allow_auto_repeat = true, |
| 6683 | 6698 | .is_noisy = true }, |
| 6684 | 6699 | |
| @@ -229,19 +229,7 @@ end · 228 unmodified lines | ||
| 229 | 229 | |
| 230 | 230 | -- Escape a string for verbatim display on the OSD |
| 231 | 231 | function ass_escape(str) |
| 232 | − -- There is no escape for '\' in ASS (I think?) but '\' is used verbatim if | |
| 233 | − -- it isn't followed by a recognised character, so add a zero-width | |
| 234 | − -- non-breaking space | |
| 235 | − str = str:gsub('\\', '\\\239\187\191') | |
| 236 | − str = str:gsub('{', '\\{') | |
| 237 | − str = str:gsub('}', '\\}') | |
| 238 | − -- Precede newlines with a ZWNBSP to prevent ASS's weird collapsing of | |
| 239 | − -- consecutive newlines | |
| 240 | − str = str:gsub('\n', '\239\187\191\\N') | |
| 241 | − -- Turn leading spaces into hard spaces to prevent ASS from stripping them | |
| 242 | − str = str:gsub('\\N ', '\\N\\h') | |
| 243 | − str = str:gsub('^ ', '\\h') | |
| 244 | − return str | |
| 232 | + return mp.command_native({'escape-ass', str}) | |
| 245 | 233 | end |
| 246 | 234 | |
| 247 | 235 | -- Takes a list of strings, a max width in characters and |
| @@ -129,9 +129,6 @@ local function graph_add_value(graph, value) · 128 unmodified lines | ||
| 129 | 129 | graph.max = max(graph.max, value) |
| 130 | 130 | end |
| 131 | 131 | |
| 132 | −-- "\\<U+2060>" in UTF-8 (U+2060 is WORD-JOINER) | |
| 133 | −local ESC_BACKSLASH = "\\" .. string.char(0xE2, 0x81, 0xA0) | |
| 134 | − | |
| 135 | 132 | local function no_ASS(t) |
| 136 | 133 | if not o.use_ass then |
| 137 | 134 | return t |
| @@ -139,16 +136,7 @@ local function no_ASS(t) · 1 unmodified line | ||
| 139 | 136 | -- mp.osd_message supports ass-escape using osd-ass-cc/{0|1} |
| 140 | 137 | return ass_stop .. t .. ass_start |
| 141 | 138 | else |
| 142 | − -- mp.set_osd_ass doesn't support ass-escape. roll our own. | |
| 143 | − -- similar to mpv's sub/osd_libass.c:mangle_ass(...), excluding | |
| 144 | − -- space after newlines because no_ASS is not used with multi-line. | |
| 145 | − -- space at the beginning is replaced with "\\h" because it matters | |
| 146 | − -- at the beginning of a line, and we can't know where our output | |
| 147 | − -- ends up. no issue if it ends up at the middle of a line. | |
| 148 | − return tostring(t) | |
| 149 | − :gsub("\\", ESC_BACKSLASH) | |
| 150 | − :gsub("{", "\\{") | |
| 151 | − :gsub("^ ", "\\h") | |
| 139 | + return mp.command_native({"escape-ass", tostring(t)}) | |
| 152 | 140 | end |
| 153 | 141 | end |
| 154 | 142 | |
| @@ -245,5 +245,6 @@ void osd_set_external(struct osd_state *osd, struct osd_external_ass *ov); · 244 unmodified lines | ||
| 245 | 245 | void osd_set_external_remove_owner(struct osd_state *osd, void *owner); |
| 246 | 246 | void osd_get_text_size(struct osd_state *osd, int *out_screen_h, int *out_font_h); |
| 247 | 247 | void osd_get_function_sym(char *buffer, size_t buffer_size, int osd_function); |
| 248 | +void osd_mangle_ass(bstr *dst, const char *in, bool replace_newlines); | |
| 248 | 249 | |
| 249 | 250 | #endif /* MPLAYER_SUB_H */ |
| @@ -193,7 +193,7 @@ void osd_get_function_sym(char *buffer, size_t buffer_size, int osd_function) · 192 unmodified lines | ||
| 193 | 193 | snprintf(buffer, buffer_size, "\xFF%c", osd_function); |
| 194 | 194 | } |
| 195 | 195 | |
| 196 | −static void mangle_ass(bstr *dst, const char *in) | |
| 196 | +void osd_mangle_ass(bstr *dst, const char *in, bool replace_newlines) | |
| 197 | 197 | { |
| 198 | 198 | const char *start = in; |
| 199 | 199 | bool escape_ass = true; |
| @@ -213,6 +213,14 @@ static void mangle_ass(bstr *dst, const char *in) · 13 unmodified lines | ||
| 213 | 213 | } |
| 214 | 214 | if (escape_ass && *in == '{') |
| 215 | 215 | bstr_xappend(NULL, dst, bstr0("\\")); |
| 216 | + // Replace newlines with \N for escape-ass. This is necessary to apply | |
| 217 | + // ASS tags past newlines and to preserve consecutive newlines with | |
| 218 | + // osd-overlay because update_external() adds a ASS event per line. | |
| 219 | + if (replace_newlines && *in == '\n') { | |
| 220 | + bstr_xappend(NULL, dst, bstr0("\\N")); | |
| 221 | + in += 1; | |
| 222 | + continue; | |
| 223 | + } | |
| 216 | 224 | // Libass will strip leading whitespace |
| 217 | 225 | if (in[0] == ' ' && (in == start || in[-1] == '\n')) { |
| 218 | 226 | bstr_xappend(NULL, dst, bstr0("\\h")); |
| @@ -231,7 +239,7 @@ static ASS_Event *add_osd_ass_event_escaped(ASS_Track *track, const char *style, · 12 unmodified lines | ||
| 231 | 239 | const char *text) |
| 232 | 240 | { |
| 233 | 241 | bstr buf = {0}; |
| 234 | − mangle_ass(&buf, text); | |
| 242 | + osd_mangle_ass(&buf, text, false); | |
| 235 | 243 | ASS_Event *e = add_osd_ass_event(track, style, buf.start); |
| 236 | 244 | talloc_free(buf.start); |
| 237 | 245 | return e; |