From e95b832747d2a0ff3400616d03240ba0d8f0d0e4 Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Wed, 2 Nov 2022 19:24:42 +0100 Subject: [PATCH] Add syntax highlighting for drawings and vector clips The highlighting distinguishes drawing commands from coordinates, and colors x and y coordinates in different colors to make coordinates easier to visually parse. Furthermore, in cubic Bezier curves, it underlines the coordinates which corresponds to endpoints of the curves. The highlighting colors are chosen to match the conventions of 3D modeling/engine software (e.g. Blender, Unity, etc), i.e. red for X and green for Y. This has the risk of being harder to tell apart for colorblind people, but all colors can be configured. --- libaegisub/ass/dialogue_parser.cpp | 118 +++++++++++++++++- .../include/libaegisub/ass/dialogue_parser.h | 13 +- src/libresrc/default_config.json | 15 ++- src/preferences.cpp | 6 +- src/subs_edit_ctrl.cpp | 13 +- tests/tests/syntax_highlight.cpp | 56 ++++++++- tests/tests/word_split.cpp | 6 +- 7 files changed, 211 insertions(+), 16 deletions(-) diff --git a/libaegisub/ass/dialogue_parser.cpp b/libaegisub/ass/dialogue_parser.cpp index e9c60ff38f..5a482f83a2 100644 --- a/libaegisub/ass/dialogue_parser.cpp +++ b/libaegisub/ass/dialogue_parser.cpp @@ -58,7 +58,11 @@ class SyntaxHighlighter { case dt::ERROR: SetStyling(tok.length, ss::ERROR); break; case dt::ARG: SetStyling(tok.length, ss::PARAMETER); break; case dt::COMMENT: SetStyling(tok.length, ss::COMMENT); break; - case dt::DRAWING: SetStyling(tok.length, ss::DRAWING); break; + case dt::DRAWING_CMD:SetStyling(tok.length, ss::DRAWING_CMD);break; + case dt::DRAWING_X: SetStyling(tok.length, ss::DRAWING_X); break; + case dt::DRAWING_Y: SetStyling(tok.length, ss::DRAWING_Y); break; + case dt::DRAWING_ENDPOINT_X: SetStyling(tok.length, ss::DRAWING_ENDPOINT_X); break; + case dt::DRAWING_ENDPOINT_Y: SetStyling(tok.length, ss::DRAWING_ENDPOINT_Y); break; case dt::TEXT: SetStyling(tok.length, ss::NORMAL); break; case dt::TAG_NAME: SetStyling(tok.length, ss::TAG); break; case dt::OPEN_PAREN: case dt::CLOSE_PAREN: case dt::ARG_SEP: case dt::TAG_START: @@ -70,6 +74,8 @@ class SyntaxHighlighter { case dt::WHITESPACE: if (ranges.size() && ranges.back().type == ss::PARAMETER) SetStyling(tok.length, ss::PARAMETER); + else if (ranges.size() && ranges.back().type == ss::DRAWING_ENDPOINT_X) + SetStyling(tok.length, ss::DRAWING_ENDPOINT_X); // connect the underline between x and y of endpoints else SetStyling(tok.length, ss::NORMAL); break; @@ -136,6 +142,66 @@ class WordSplitter { } } + void SplitDrawing(size_t &i) { + size_t starti = i; + + // First, split into words + size_t dpos = pos; + size_t tlen = 0; + bool tokentype = text[pos] == ' ' || text[pos] == '\t'; + while (tlen < tokens[i].length) { + bool newtype = text[dpos] == ' ' || text[dpos] == '\t'; + if (newtype != tokentype) { + tokentype = newtype; + SwitchTo(i, tokentype ? dt::DRAWING_FULL : dt::WHITESPACE, tlen); + tokens[i].type = tokentype ? dt::WHITESPACE : dt::DRAWING_FULL; + tlen = 0; + } + ++tlen; + ++dpos; + } + + // Then, label all the tokens + dpos = pos; + int num_coord = 0; + char lastcmd = ' '; + + for (size_t j = starti; j <= i; j++) { + char c = text[dpos]; + if (tokens[j].type == dt::WHITESPACE) { + } else if (lastcmd == ' ' && c != 'm') { + tokens[j].type = dt::ERROR; + } else if (c == 'm' || c == 'n' || c == 'l' || c == 's' || c == 'b' || c == 'p' || c == 'c') { + tokens[j].type = dt::DRAWING_CMD; + + if (tokens[j].length != 1) + tokens[j].type = dt::ERROR; + if (num_coord % 2 != 0) + tokens[j].type = dt::ERROR; + + lastcmd = c; + num_coord = 0; + } else { + bool valid = true; + for (size_t k = 0; k < tokens[j].length; k++) { + char c = text[dpos + k]; + if (!((c >= '0' && c <= '9') || c == '.' || c == '-' || c == 'e')) { + valid = false; + } + } + if (!valid) + tokens[j].type = dt::ERROR; + else if (lastcmd == 'b' && num_coord % 6 >= 4) + tokens[j].type = num_coord % 2 == 0 ? dt::DRAWING_ENDPOINT_X : dt::DRAWING_ENDPOINT_Y; + else + tokens[j].type = num_coord % 2 == 0 ? dt::DRAWING_X : dt::DRAWING_Y; + ++num_coord; + } + + dpos += tokens[j].length; + } + } + public: WordSplitter(std::string_view text, std::vector &tokens) : text(text) @@ -149,6 +215,9 @@ class WordSplitter { size_t len = tokens[i].length; if (tokens[i].type == dt::TEXT) SplitText(i); + else if (tokens[i].type == dt::DRAWING_FULL) { + SplitDrawing(i); + } pos += len; } } @@ -182,9 +251,52 @@ void MarkDrawings(std::string_view str, std::vector &tokens) { switch (tokens[i].type) { case dt::TEXT: if (in_drawing) - tokens[i].type = dt::DRAWING; + tokens[i].type = dt::DRAWING_FULL; break; case dt::TAG_NAME: + // Mark vector clip arguments as drawings + if (i + 3 < tokens.size() && (len == 4 || len == 5) && str.substr(pos, len).ends_with("clip")) { + if (tokens[i + 1].type != dt::OPEN_PAREN) + goto tag_p; + + size_t drawing_start = 0; + size_t drawing_end = 0; + + // Try to find a vector clip + for (size_t j = i + 2; j < tokens.size(); j++) { + if (tokens[j].type == dt::ARG_SEP) { + if (drawing_start) { + break; // More than two arguents - this is a rectangular clip + } + drawing_start = j + 1; + } else if (tokens[j].type == dt::CLOSE_PAREN) { + drawing_end = j; + break; + } else if (tokens[j].type != dt::WHITESPACE && tokens[j].type != dt::ARG) { + break; + } + } + + if (!drawing_end) + goto tag_p; + if (!drawing_start) + drawing_start = i + 2; + if (drawing_end == drawing_start) + goto tag_p; + + // We found a clip between drawing_start and drawing_end. Now, join + // all the tokens into one and label it as a drawing. + size_t tokenlen = 0; + for (size_t j = drawing_start; j < drawing_end; j++) { + tokenlen += tokens[j].length; + } + + tokens[drawing_start].length = tokenlen; + tokens[drawing_start].type = dt::DRAWING_FULL; + tokens.erase(tokens.begin() + drawing_start + 1, tokens.begin() + drawing_end); + last_ovr_end -= drawing_end - drawing_start - 1; + } +tag_p: if (len != 1 || i + 1 >= tokens.size() || str[pos] != 'p') break; @@ -218,7 +330,7 @@ void MarkDrawings(std::string_view str, std::vector &tokens) { case dt::KARAOKE_VARIABLE: break; case dt::LINE_BREAK: break; default: - tokens[i].type = in_drawing ? dt::DRAWING : dt::TEXT; + tokens[i].type = in_drawing ? dt::DRAWING_FULL : dt::TEXT; if (i > 0 && tokens[i - 1].type == tokens[i].type) { tokens[i - 1].length += tokens[i].length; tokens.erase(tokens.begin() + i); diff --git a/libaegisub/include/libaegisub/ass/dialogue_parser.h b/libaegisub/include/libaegisub/ass/dialogue_parser.h index 19c3f61b77..6d051fc12b 100644 --- a/libaegisub/include/libaegisub/ass/dialogue_parser.h +++ b/libaegisub/include/libaegisub/ass/dialogue_parser.h @@ -39,7 +39,12 @@ namespace agi { ERROR, COMMENT, WHITESPACE, - DRAWING, + DRAWING_FULL, + DRAWING_CMD, + DRAWING_X, + DRAWING_Y, + DRAWING_ENDPOINT_X, + DRAWING_ENDPOINT_Y, KARAOKE_TEMPLATE, KARAOKE_VARIABLE }; @@ -49,7 +54,11 @@ namespace agi { enum { NORMAL = 0, COMMENT, - DRAWING, + DRAWING_CMD, + DRAWING_X, + DRAWING_Y, + DRAWING_ENDPOINT_X, + DRAWING_ENDPOINT_Y, OVERRIDE, PUNCTUATION, TAG, diff --git a/src/libresrc/default_config.json b/src/libresrc/default_config.json index 640baef421..6991027175 100644 --- a/src/libresrc/default_config.json +++ b/src/libresrc/default_config.json @@ -229,7 +229,9 @@ "Background" : { "Brackets" : "", "Comment" : "", - "Drawing" : "", + "Drawing Command" : "", + "Drawing X" : "", + "Drawing Y" : "", "Error" : "rgb(255, 200, 200)", "Karaoke Template" : "", "Karaoke Variable" : "", @@ -242,7 +244,9 @@ "Bold" : { "Brackets" : false, "Comment" : true, - "Drawing" : true, + "Drawing Command" : true, + "Drawing X" : false, + "Drawing Y" : false, "Error" : false, "Karaoke Template" : true, "Karaoke Variable" : true, @@ -252,9 +256,14 @@ "Slashes" : false, "Tags" : true }, + "Underline": { + "Drawing Endpoint": true + }, "Brackets" : "rgb(20, 50, 255)", "Comment" : "rgb(0,0,0)", - "Drawing" : "rgb(0,0,0)", + "Drawing Command" : "rgb(0,0,0)", + "Drawing X" : "rgb(90,40,40)", + "Drawing Y" : "rgb(40,90,40)", "Error" : "rgb(200, 0, 0)", "Karaoke Template" : "rgb(128, 0, 192)", "Karaoke Variable" : "rgb(128, 0, 192)", diff --git a/src/preferences.cpp b/src/preferences.cpp index a9f73132d3..f299fa06ef 100644 --- a/src/preferences.cpp +++ b/src/preferences.cpp @@ -253,7 +253,11 @@ void Interface_Colours(wxTreebook *book, Preferences *parent) { p->OptionAdd(syntax, _("Background"), "Colour/Subtitle/Background"); p->OptionAdd(syntax, _("Normal"), "Colour/Subtitle/Syntax/Normal"); p->OptionAdd(syntax, _("Comments"), "Colour/Subtitle/Syntax/Comment"); - p->OptionAdd(syntax, _("Drawings"), "Colour/Subtitle/Syntax/Drawing"); + p->OptionAdd(syntax, _("Drawing Commands"), "Colour/Subtitle/Syntax/Drawing Command"); + p->OptionAdd(syntax, _("Drawing X Coords"), "Colour/Subtitle/Syntax/Drawing X"); + p->OptionAdd(syntax, _("Drawing Y Coords"), "Colour/Subtitle/Syntax/Drawing Y"); + p->OptionAdd(syntax, _("Underline Spline Endpoints"), "Colour/Subtitle/Syntax/Underline/Drawing Endpoint"); + p->CellSkip(syntax); p->OptionAdd(syntax, _("Brackets"), "Colour/Subtitle/Syntax/Brackets"); p->OptionAdd(syntax, _("Slashes and Parentheses"), "Colour/Subtitle/Syntax/Slashes"); p->OptionAdd(syntax, _("Tags"), "Colour/Subtitle/Syntax/Tags"); diff --git a/src/subs_edit_ctrl.cpp b/src/subs_edit_ctrl.cpp index 535e3a5dcf..cbdb1fd621 100644 --- a/src/subs_edit_ctrl.cpp +++ b/src/subs_edit_ctrl.cpp @@ -138,7 +138,10 @@ SubsTextEditCtrl::SubsTextEditCtrl(wxWindow* parent, wxSize wsize, long style, a OPT_SUB("Subtitle/Edit Box/Font Size", &SubsTextEditCtrl::SetStyles, this); Subscribe("Normal"); Subscribe("Comment"); - Subscribe("Drawing"); + Subscribe("Drawing Command"); + Subscribe("Drawing X"); + Subscribe("Drawing Y"); + OPT_SUB("Colour/Subtitle/Syntax/Underline/Drawing Endpoint", &SubsTextEditCtrl::SetStyles, this); Subscribe("Brackets"); Subscribe("Slashes"); Subscribe("Tags"); @@ -230,7 +233,13 @@ void SubsTextEditCtrl::SetStyles() { namespace ss = agi::ass::SyntaxStyle; SetSyntaxStyle(ss::NORMAL, font, "Normal", default_background); SetSyntaxStyle(ss::COMMENT, font, "Comment", default_background); - SetSyntaxStyle(ss::DRAWING, font, "Drawing", default_background); + SetSyntaxStyle(ss::DRAWING_CMD, font, "Drawing Command", default_background); + SetSyntaxStyle(ss::DRAWING_X, font, "Drawing X", default_background); + SetSyntaxStyle(ss::DRAWING_Y, font, "Drawing Y", default_background); + SetSyntaxStyle(ss::DRAWING_ENDPOINT_X, font, "Drawing X", default_background); + SetSyntaxStyle(ss::DRAWING_ENDPOINT_Y, font, "Drawing Y", default_background); + StyleSetUnderline(ss::DRAWING_ENDPOINT_X, OPT_GET("Colour/Subtitle/Syntax/Underline/Drawing Endpoint")->GetBool()); + StyleSetUnderline(ss::DRAWING_ENDPOINT_Y, OPT_GET("Colour/Subtitle/Syntax/Underline/Drawing Endpoint")->GetBool()); SetSyntaxStyle(ss::OVERRIDE, font, "Brackets", default_background); SetSyntaxStyle(ss::PUNCTUATION, font, "Slashes", default_background); SetSyntaxStyle(ss::TAG, font, "Tags", default_background); diff --git a/tests/tests/syntax_highlight.cpp b/tests/tests/syntax_highlight.cpp index 482bf0842a..6f5a114cfa 100644 --- a/tests/tests/syntax_highlight.cpp +++ b/tests/tests/syntax_highlight.cpp @@ -74,14 +74,47 @@ TEST(lagi_syntax, spellcheck) { } TEST(lagi_syntax, drawing) { - tok_str("incorrect{\\p1}m 10 10{\\p}correct", false, + tok_str("incorrect{\\clip(m 10 10 l 20 20 c)\\p1}m 10 10 b 0 0 0 100 100 0{\\p}correct", false, expect_style(ss::SPELLING, 9u); expect_style(ss::OVERRIDE, 1u); expect_style(ss::PUNCTUATION, 1u); + expect_style(ss::TAG, 4u); + expect_style(ss::PUNCTUATION, 1u); + expect_style(ss::DRAWING_CMD, 1u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_X, 2u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_Y, 2u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_CMD, 1u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_X, 2u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_Y, 2u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_CMD, 1u); + expect_style(ss::PUNCTUATION, 2u); expect_style(ss::TAG, 1u); expect_style(ss::PARAMETER, 1u); expect_style(ss::OVERRIDE, 1u); - expect_style(ss::DRAWING, 7u); + expect_style(ss::DRAWING_CMD, 1u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_X, 2u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_Y, 2u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_CMD, 1u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_X, 1u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_Y, 1u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_X, 1u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_Y, 3u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_ENDPOINT_X, 4u); + expect_style(ss::DRAWING_ENDPOINT_Y, 1u); expect_style(ss::OVERRIDE, 1u); expect_style(ss::PUNCTUATION, 1u); expect_style(ss::TAG, 1u); @@ -90,6 +123,25 @@ TEST(lagi_syntax, drawing) { ); } +TEST(lagi_syntax, drawing_without_m) { + tok_str("{\\p1}l 100 100 0 100", false, + expect_style(ss::OVERRIDE, 1u); + expect_style(ss::PUNCTUATION, 1u); + expect_style(ss::TAG, 1u); + expect_style(ss::PARAMETER, 1u); + expect_style(ss::OVERRIDE, 1u); + expect_style(ss::ERROR, 1u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::ERROR, 3u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::ERROR, 3u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::ERROR, 1u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::ERROR, 3u); + ); +} + TEST(lagi_syntax, transform) { tok_str("{\\t(0, 0, \\clip(0,0,10,10)}clipped text", false, expect_style(ss::OVERRIDE, 1u); diff --git a/tests/tests/word_split.cpp b/tests/tests/word_split.cpp index c19c45e285..f09f87c489 100644 --- a/tests/tests/word_split.cpp +++ b/tests/tests/word_split.cpp @@ -108,12 +108,12 @@ TEST(lagi_word_split, drawing) { SplitWords(text, tokens); - ASSERT_EQ(15u, tokens.size()); + ASSERT_EQ(17u, tokens.size()); EXPECT_EQ(dt::WORD, tokens[0].type); EXPECT_EQ(dt::WORD, tokens[2].type); - EXPECT_EQ(dt::WORD, tokens[14].type); + EXPECT_EQ(dt::WORD, tokens[16].type); - EXPECT_EQ(dt::DRAWING, tokens[8].type); + EXPECT_EQ(dt::DRAWING_CMD, tokens[8].type); } TEST(lagi_word_split, unclosed_ovr) {