highlightjs-ass
Version:
(Advanced) SubStation Alpha subtitle grammar for highlight.js
527 lines (505 loc) • 12 kB
JavaScript
/*
Language: Advanced SubStation Alpha
Author: GrygrFlzr <git@cybeast.dev>
Category: subtitle
Website: https://github.com/libass/libass
*/
/**
* @typedef {import('highlight.js').Mode} Mode
*/
/** @type {import('highlight.js').LanguageFn} */
export default function (hljs) {
const regex = hljs.regex;
const INTEGERS = regex.either(
...[
// integer
/-?\d+/,
// scientific integer
/-?\d*[Ee]\d*/,
]
);
const DECIMALS = regex.either(
...[
// float
/-?\d*\.\d+/, // e.g. .4
/-?\d+\.\d*/, // e.g. 4.
// scientific float - no decimal after E
/-?\d*\.\d*[Ee]\d*/,
]
);
const ALPHAS = /&H[0-9A-Fa-f]{2}&/;
const COLORS = /&H[0-9A-Fa-f]{3,8}&/;
/** @type {Mode} */
const DECIMAL_TAGS = {
begin: [
/\\/,
regex.either(
...[
"(?:xbord|ybord|xshad|yshad|bord|shad|blur|fscx|fscy|fsc)",
"(?:pbo|frx|fry|frz|fax|fay|fsp|kf|ko|kt|fe|be|fr|fs|K|k)",
]
),
regex.either(DECIMALS, INTEGERS),
],
beginScope: {
1: "title.function.invoke",
2: "title.function.invoke",
3: "number",
},
};
/** @type {Mode} */
const INTEGER_TAGS = {
begin: /\\(an|b|q|a(?!lpha))/,
beginScope: "title.function.invoke",
end: regex.concat("(?:", INTEGERS, ")?", /(?=[\}\\]|$)/),
endScope: "number",
};
/** @type {Mode} */
const BOOLEAN_TAGS = {
begin: /\\(i(?!clip)|s|u)/,
beginScope: "title.function.invoke",
end: regex.concat(/[01]?/, /(?=[\}\\]|$)/),
endScope: "number",
};
/** @type {Mode} */
const ALPHA_TAGS = {
begin: /\\(alpha|1a|2a|3a|4a)/,
beginScope: "title.function.invoke",
end: regex.concat("(?:", ALPHAS, ")?", /(?=[\}\\]|$)/),
endScope: "literal",
relevance: 5,
};
/** @type {Mode} */
const COLOR_TAGS = {
begin: /\\(1c|2c|3c|4c|c(?!lip))/,
beginScope: "title.function.invoke",
end: regex.concat("(?:", COLORS, ")?", /(?=[\}\\]|$)/),
endScope: "literal",
relevance: 5,
};
/** @type {Mode} */
const FUNCTION_TAGS = {
begin: [/\\(move|fade|fad|org|pos)/, /\(/, /[^\)]*/, /\)/],
beginScope: {
1: "title.function.invoke",
3: "literals",
},
end: /(?=[\}\\]|$)/,
relevance: 2,
};
/** @type {Mode} */
const STRING_TAGS = {
begin: [/\\(fn|r)/, /[^\\\}]*/],
beginScope: {
1: "title.function.invoke",
2: "string",
},
end: /(?=[\}\\]|$)/,
};
/** @type {Mode} */
const COMMENTS = {
variants: [
hljs.COMMENT(/^;/, "$"),
hljs.COMMENT(/^Data:\s+/, "$"), // aegisub extradata
hljs.COMMENT(/^Comment:\s+/, "$"),
hljs.COMMENT(/^Format:\s+/, "$"),
// SSA 3.x comments
hljs.COMMENT(/^!: /, "$"),
],
relevance: 5,
};
/** @type {Mode} */
const TRANSFORM_TAGS = {
scope: "string",
begin: [/\\t/, /\(/],
beginScope: {
1: "title.function.invoke",
},
starts: {
end: /\)|$/,
excludeEnd: true,
scope: "formula",
contains: [
DECIMAL_TAGS,
INTEGER_TAGS,
BOOLEAN_TAGS,
ALPHA_TAGS,
COLOR_TAGS,
FUNCTION_TAGS,
STRING_TAGS,
],
},
};
/** @type {Mode} */
const DRAWING_COMMANDS = {
begin: [
/[mnlbspc]? /,
regex.either(DECIMALS, INTEGERS),
/ /,
regex.either(DECIMALS, INTEGERS),
],
beginScope: {
1: "built_in",
2: "link",
4: "literal",
},
};
/** @type {Mode} */
const CLIP_TAGS = {
begin: [/\\i?clip/, /\(/],
beginScope: {
1: "built_in",
},
starts: {
end: /[)}]|$/,
contains: [
// vector version
DRAWING_COMMANDS,
// rect version
{
match: regex.either(DECIMALS, INTEGERS),
scope: "number",
},
],
},
relevance: 5,
};
/** @type {Mode} */
const DRAWING_MODE = {
begin: /\\p[1-4]/,
beginScope: "built_in",
starts: {
end: /\\p0\}?|$/,
endsParent: true,
contains: [
DRAWING_COMMANDS,
// these aren't _actually_ part of the \p1 tag
// but for parsing ease we'll just repeat them
CLIP_TAGS,
DECIMAL_TAGS,
INTEGER_TAGS,
BOOLEAN_TAGS,
ALPHA_TAGS,
COLOR_TAGS,
FUNCTION_TAGS,
STRING_TAGS,
],
},
};
/** @type {Mode} */
const BRACE_GROUP = {
scope: "comment",
begin: /\{/,
end: regex.either(/\}/, /$/),
contains: [
CLIP_TAGS,
DECIMAL_TAGS,
INTEGER_TAGS,
BOOLEAN_TAGS,
ALPHA_TAGS,
COLOR_TAGS,
FUNCTION_TAGS,
STRING_TAGS,
// Special cases
TRANSFORM_TAGS,
DRAWING_MODE,
],
relevance: 0,
};
/** @type {Mode} */
const SECTION_HEADER = {
scope: "section",
begin: /^\[/,
end: /\]/,
relevance: 0,
};
// \n, \N, and \h
/** @type {Mode} */
const SIMPLE_ESCAPE_CHAR = {
match: /\\[nNh]/,
scope: "literal",
relevance: 0,
};
/**
* Modern ASS or SSA 4.x dialogue
* @type {Mode}
**/
const DIALOGUE = {
begin: [
/^Dialogue/, // Key
/:[\t ]/, // Strictly singular space or tab for performance
/\d{0,3}/, // Layer in ASS and SSA 4
/,/,
/[:\d.]+/, // Start
/,/,
/[:\d.]+/, // End
/,/,
/[^,\r\n]+/, // Style
/,/,
/[^,\r\n]*/, // Actor
/,/,
// Limit to 4 digits as that was what Sub Station generated
/\d{0,4}/, // MarginL
/,/,
/\d{0,4}/, // MarginR
/,/,
/\d{0,4}/, // MarginV
/,/,
/[^,\r\n]*/, // Effect
/,/,
],
beginScope: {
1: "built_in", // Key
2: "comment",
3: "number", // Layer
4: "comment",
5: "literal", // Start
6: "comment",
7: "title.class", // End
8: "comment",
9: "variable.constant", // Style
10: "comment",
11: "variable", // Actor Name
12: "comment",
13: "comment", // MarginL
14: "comment",
15: "comment", // MarginR
16: "comment",
17: "comment", // MarginV
18: "comment",
19: "comment", // Effect
20: "comment",
},
starts: {
end: /$/,
contains: [SIMPLE_ESCAPE_CHAR, BRACE_GROUP],
},
relevance: 10,
};
/** @type {Mode} */
const SSA_2_OR_3_DIALOGUE = {
begin: [
/^Dialogue/, // Key
/:[\t ]/, // Strictly singular space or tab for performance
/Marked=\d/, // Layer in ASS
/,/,
/[:\d.]+/, // Start
/,/,
/[:\d.]+/, // End
/,/,
/[^,\r\n]+/, // Style
/,/,
/[^,\r\n]*/, // Actor
/,/,
/\d{4}/, // MarginL
/,/,
/\d{4}/, // MarginR
/,/,
/\d{4}/, // MarginV
/,/,
// Effect is stricter in SSA 2 and 3
// as other authoring tools were not in much use
/(![Ee]ffect|[Kk]araoke|[Ss]croll ?[Uu]p|)/,
/,/,
],
beginScope: {
1: "built_in", // Key
2: "comment",
3: "number", // Layer
4: "comment",
5: "literal", // Start
6: "comment",
7: "title.class", // End
8: "comment",
9: "variable.constant", // Style
10: "comment",
11: "variable", // Actor Name
12: "comment",
13: "comment", // MarginL
14: "comment",
15: "comment", // MarginR
16: "comment",
17: "comment", // MarginV
18: "comment",
19: "built_in", // Effect
20: "comment",
},
starts: {
end: /$/,
contains: [SIMPLE_ESCAPE_CHAR, BRACE_GROUP],
},
relevance: 10,
};
/**
* \C1, \C3 - \C7 tags seen in SSA 1 scripts
* Also \CF
* @type {Mode}
*/
const SSA_1_COLOR = {
begin: /\\C/,
end: /[\dF]/,
beginScope: "title.function.invoke",
endScope: "title.function.invoke",
};
/**
* \F1 and \F2 tags seen in SSA 1 scripts
* @type {Mode}
*/
const SSA_1_F_TAG = {
begin: /\\F/,
end: /[12]/,
beginScope: "title.function.invoke",
endScope: "title.function.invoke",
};
/**
* \I tags seen in SSA 1 scripts
* @type {Mode}
*/
const SSA_1_ITALICS = {
begin: /\\I/,
beginScope: "title.function.invoke",
};
/** @type {Mode} */
const SSA_1_DIALOGUE = {
begin: [
/^Dialogue/, // Key
/:[\t ]/, // Strictly singular space or tab for performance
/Marked=\d/, // Marked in SSA 1.x
/,/,
/[:\d.]+/, // Start
/,/,
/[:\d.]+/, // End
/,/,
/[^,\r\n]+/, // Style
/,/,
/[^,\r\n]*/, // Actor
/,/,
/\d+/, // MarginL
/,/,
/\d+/, // MarginR
/,/,
/\d+/, // MarginV
/,(?!!Effect,)/,
// No effect field, it was added in v2+
],
beginScope: {
1: "built_in", // Key
2: "comment",
3: "number", // Layer
4: "comment",
5: "literal", // Start
6: "comment",
7: "title.class", // End
8: "comment",
9: "variable.constant", // Style
10: "comment",
11: "variable", // Actor Name
12: "comment",
13: "comment", // MarginL
14: "comment",
15: "comment", // MarginR
16: "comment",
17: "comment", // MarginV
18: "comment",
},
starts: {
end: /$/,
contains: [
SIMPLE_ESCAPE_CHAR,
SSA_1_COLOR,
SSA_1_F_TAG,
SSA_1_ITALICS,
BRACE_GROUP,
],
},
relevance: 10,
};
/** @type {Mode} */
const STYLE_DEFINITION = {
begin: [
/^Style/, // Key
/:[\t ]/, // Strictly singular space or tab for performance
/[^,\r\n]+/, // Name
/,/,
/[^,\r\n]+/, // Font Name
/,/,
/\d{1,8}/, // Font Size
/,/,
// Colors are hex in ASS and signed ints in SSA
/(&H[0-9A-Fa-f]{2,8}&?|-?\d+)/, // Primary Color
/,/,
/(&H[0-9A-Fa-f]{2,8}&?|-?\d+)/, // Secondary Color
/,/,
/(&H[0-9A-Fa-f]{2,8}&?|-?\d+)/, // Outline Color
/,/,
/(&H[0-9A-Fa-f]{2,8}&?|-?\d+)/, // Background Color
/,/,
// We don't care about:
// Bold, Italic, Underline, Strikethrough, ScaleX, ScaleY, Spacing
// Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR
// MarginV, Encoding
/[\d+,.-]+$/, // Allow negative and decimal values
],
beginScope: {
1: "attr", // Key
2: "comment",
3: "title.class", // Name
4: "comment",
5: "title.class", // Font Name
6: "comment",
7: "number", // Font Size
8: "comment",
9: "literal", // Primary Color
10: "comment",
11: "literal", // Secondary Color
12: "comment",
13: "literal", // Outline Color
14: "comment",
15: "literal", // Background Color
16: "comment",
// We don't care about:
// Bold, Italic, Underline, Strikethrough, ScaleX, ScaleY, Spacing
// Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR
// MarginV, Encoding
17: "comment",
},
relevance: 10,
};
// A simple key begins with an alphabet at the start of the line
// ...followed by any number of alphabetical letters or spaces
// ...ending with a colon followed immediately by a space, tab or newline.
// EXCEPT Dialogue, Style, Format, Comment, or Data lines
/** @type {Mode} */
const SIMPLE_KEY_VALUE = {
begin: [
/^(?!Dialogue|Style|Format|Comment|Data)[A-Za-z][A-Za-z \t]*/,
/:[\t ]*/,
],
beginScope: {
1: "attr",
},
starts: {
scope: "string",
end: /$/,
},
relevance: 0,
};
return {
name: "Advanced SubStation Alpha",
aliases: ["ssa"],
case_insensitive: false,
contains: [
// Partial ASS tags
BRACE_GROUP,
SIMPLE_ESCAPE_CHAR,
// completely defined ASS-files
SECTION_HEADER,
COMMENTS,
SIMPLE_KEY_VALUE,
DIALOGUE,
SSA_2_OR_3_DIALOGUE,
SSA_1_DIALOGUE,
STYLE_DEFINITION,
],
};
}