Skip to content

Commit

Permalink
Add syntax highlighting for drawings and vector clips
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
arch1t3cht committed Jan 17, 2025
1 parent 112aadf commit e95b832
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 16 deletions.
118 changes: 115 additions & 3 deletions libaegisub/ass/dialogue_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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;
Expand Down Expand Up @@ -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<DialogueToken> &tokens)
: text(text)
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -182,9 +251,52 @@ void MarkDrawings(std::string_view str, std::vector<DialogueToken> &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;

Expand Down Expand Up @@ -218,7 +330,7 @@ void MarkDrawings(std::string_view str, std::vector<DialogueToken> &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);
Expand Down
13 changes: 11 additions & 2 deletions libaegisub/include/libaegisub/ass/dialogue_parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand All @@ -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,
Expand Down
15 changes: 12 additions & 3 deletions src/libresrc/default_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,9 @@
"Background" : {
"Brackets" : "",
"Comment" : "",
"Drawing" : "",
"Drawing Command" : "",
"Drawing X" : "",
"Drawing Y" : "",
"Error" : "rgb(255, 200, 200)",
"Karaoke Template" : "",
"Karaoke Variable" : "",
Expand All @@ -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,
Expand All @@ -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)",
Expand Down
6 changes: 5 additions & 1 deletion src/preferences.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
13 changes: 11 additions & 2 deletions src/subs_edit_ctrl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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);
Expand Down
56 changes: 54 additions & 2 deletions tests/tests/syntax_highlight.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions tests/tests/word_split.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit e95b832

Please sign in to comment.