From f9e3b8ba7da220746151179ed10a90c2ac215ffc Mon Sep 17 00:00:00 2001 From: frosty Date: Mon, 17 Aug 2026 16:32:52 -0400 Subject: Begin implementing codeblocks feature --- main/data/style.css | 32 +++ main/meson.build | 2 + .../code_block_widget.vala | 224 ++++++++++++++++++++ .../conversation_content_view/message_widget.vala | 235 ++++++++++++++------- .../syntax_highlighting.vala | 136 ++++++++++++ 5 files changed, 555 insertions(+), 74 deletions(-) create mode 100644 main/src/ui/conversation_content_view/code_block_widget.vala create mode 100644 main/src/ui/conversation_content_view/syntax_highlighting.vala diff --git a/main/data/style.css b/main/data/style.css index 1700a624..fc5915fb 100644 --- a/main/data/style.css +++ b/main/data/style.css @@ -120,6 +120,38 @@ window.dino-main .dino-quote:hover { background: alpha(@theme_fg_color, 0.08); } +window.dino-main .code-block { + background: alpha(@theme_fg_color, 0.06); + border: 1px solid alpha(@theme_fg_color, 0.12); + border-radius: 8px; +} + +window.dino-main .code-block-header { + background: alpha(@theme_fg_color, 0.06); + border-bottom: 1px solid alpha(@theme_fg_color, 0.1); + padding: 4px 6px 4px 12px; +} + +window.dino-main .code-block-language { + font-weight: bold; +} + +window.dino-main .code-block-meta { + color: alpha(@theme_fg_color, 0.65); + font-size: 0.85em; +} + +window.dino-main .code-block-download { + min-height: 24px; + min-width: 24px; + padding: 0; +} + +window.dino-main .code-block-content { + font-family: monospace; + padding: 10px 12px 12px; +} + picture.avatar { border-radius: 3px; } diff --git a/main/meson.build b/main/meson.build index 38c1a733..0f0c29f3 100644 --- a/main/meson.build +++ b/main/meson.build @@ -42,6 +42,8 @@ sources = files( 'src/ui/contact_details/settings_provider.vala', 'src/ui/conversation_content_view/call_widget.vala', 'src/ui/conversation_content_view/chat_state_populator.vala', + 'src/ui/conversation_content_view/syntax_highlighting.vala', + 'src/ui/conversation_content_view/code_block_widget.vala', 'src/ui/conversation_content_view/content_populator.vala', 'src/ui/conversation_content_view/conversation_item_skeleton.vala', 'src/ui/conversation_content_view/conversation_view.vala', diff --git a/main/src/ui/conversation_content_view/code_block_widget.vala b/main/src/ui/conversation_content_view/code_block_widget.vala new file mode 100644 index 00000000..dc1ecc1f --- /dev/null +++ b/main/src/ui/conversation_content_view/code_block_widget.vala @@ -0,0 +1,224 @@ +using Gee; +using Gdk; +using Gtk; +using Pango; + +namespace Dino.Ui.ConversationSummary { + +public class CodeBlockPart : Object { + public bool is_code; + public string text; + public string language; + public int source_start_char; + + public CodeBlockPart.from_text(string text, int source_start_char) { + this.text = text; + this.source_start_char = source_start_char; + this.language = ""; + } + + public CodeBlockPart.from_code(string text, string language) { + this.is_code = true; + this.text = text; + this.language = language; + } +} + +public class CodeBlockParser : Object { + private static Regex? fence_regex; + + public static ArrayList parse(string text, out bool has_blocks) { + var parts = new ArrayList(); + Regex regex = get_fence_regex(); + MatchInfo match_info; + int offset = 0; + has_blocks = false; + try { + regex.match_full(text, -1, offset, 0, out match_info); + } catch (RegexError error) { + parts.add(new CodeBlockPart.from_text(text, 0)); + return parts; + } + while (match_info.matches()) { + int match_start; + int match_end; + int code_start; + int code_end; + match_info.fetch_pos(0, out match_start, out match_end); + match_info.fetch_pos(2, out code_start, out code_end); + int text_end = match_start; + if (text_end > offset && text[text_end - 1] == '\n') { + text_end--; + if (text_end > offset && text[text_end - 1] == '\r') text_end--; + } + if (text_end > offset) { + parts.add(new CodeBlockPart.from_text(text.substring(offset, text_end - offset), text.substring(0, offset).char_count())); + } + if (code_end > code_start && text[code_end - 1] == '\n') { + code_end--; + if (code_end > code_start && text[code_end - 1] == '\r') code_end--; + } + string code = text.substring(code_start, code_end - code_start).replace("\r\n", "\n"); + parts.add(new CodeBlockPart.from_code(code, match_info.fetch(1).strip())); + has_blocks = true; + offset = match_end; + if (offset >= text.length) break; + try { + regex.match_full(text, -1, offset, 0, out match_info); + } catch (RegexError error) { + break; + } + } + if (offset < text.length) { + parts.add(new CodeBlockPart.from_text(text.substring(offset), text.substring(0, offset).char_count())); + } + return parts; + } + + private static Regex get_fence_regex() { + if (fence_regex == null) { + try { + fence_regex = new Regex("(?ms)^```[\\t ]*([^\\r\\n`]*)[\\t ]*\\r?\\n(.*?)^```[\\t ]*(?:\\r?\\n|$)", RegexCompileFlags.OPTIMIZE); + } catch (RegexError error) { + GLib.error("Invalid code block expression: %s", error.message); + } + } + return fence_regex; + } +} + +public class CodeBlockWidget : Box { + private string code; + private CodeSyntax.LanguageDefinition? definition; + private Label code_label; + private ulong realize_id; + + public CodeBlockWidget(string code, string language) { + Object(orientation: Orientation.VERTICAL, spacing: 0, halign: Align.START, overflow: Overflow.HIDDEN); + this.code = code; + definition = CodeSyntax.find(language); + add_css_class("code-block"); + + var header = new Box(Orientation.HORIZONTAL, 8); + header.add_css_class("code-block-header"); + + string language_name = definition != null ? definition.name : language.strip(); + if (language_name == "") language_name = _("Plain text"); + var language_label = new Label(language_name) { xalign=0, hexpand=true }; + language_label.add_css_class("code-block-language"); + header.append(language_label); + + int line_count = code == "" ? 0 : code.split("\n").length; + var line_label = new Label(n("%d line", "%d lines", line_count).printf(line_count)); + line_label.add_css_class("code-block-meta"); + header.append(line_label); + + var download_button = new Button.from_icon_name("dino-file-download-symbolic") { + tooltip_text=_("Download code") + }; + download_button.add_css_class("flat"); + download_button.add_css_class("code-block-download"); + download_button.clicked.connect(save); + header.append(download_button); + append(header); + + code_label = new Label(code) { + use_markup=false, + selectable=true, + xalign=0, + yalign=0, + wrap=false + }; + code_label.add_css_class("code-block-content"); + append(code_label); + + apply_highlighting(); + realize_id = realize.connect(apply_highlighting); + } + + private void apply_highlighting() { + var attributes = new AttrList(); + if (definition != null) { + foreach (CodeSyntax.HighlightSpan span in definition.highlight(code)) { + RGBA color = get_token_color(span.style); + Attribute foreground = Pango.attr_foreground_new( + (uint16) Math.round(color.red * uint16.MAX), + (uint16) Math.round(color.green * uint16.MAX), + (uint16) Math.round(color.blue * uint16.MAX) + ); + foreground.start_index = span.start; + foreground.end_index = span.end; + attributes.insert(foreground.copy()); + if (color.alpha < 1) { + Attribute alpha = Pango.attr_foreground_alpha_new((uint16) Math.round(color.alpha * uint16.MAX)); + alpha.start_index = span.start; + alpha.end_index = span.end; + attributes.insert(alpha.copy()); + } + } + } + code_label.attributes = attributes; + } + + private RGBA get_token_color(CodeSyntax.TokenStyle style) { + StyleContext context = code_label.get_style_context(); + RGBA color = context.get_color(); + string? color_name = null; + switch (style) { + case CodeSyntax.TokenStyle.COMMENT: + color.alpha = 0.62f; + break; + case CodeSyntax.TokenStyle.CONSTANT: + color_name = "error_color"; + break; + case CodeSyntax.TokenStyle.NUMBER: + case CodeSyntax.TokenStyle.PREPROCESSOR: + color_name = "warning_color"; + break; + case CodeSyntax.TokenStyle.STRING: + case CodeSyntax.TokenStyle.TYPE: + color_name = "success_color"; + break; + case CodeSyntax.TokenStyle.FUNCTION: + case CodeSyntax.TokenStyle.KEYWORD: + color_name = "accent_color"; + break; + } + if (color_name != null) { + RGBA theme_color; + if (context.lookup_color(color_name, out theme_color)) color = theme_color; + } + return color; + } + + private void save() { + var dialog = new FileChooserNative(_("Save code as…"), get_root() as Gtk.Window, FileChooserAction.SAVE, _("Save"), _("Cancel")); + dialog.modal = true; + dialog.set_current_name("code." + (definition != null ? definition.extension : "txt")); + dialog.response.connect((response_id) => { + if (response_id == ResponseType.ACCEPT) { + File? target = dialog.get_file(); + if (target != null) { + try { + string? etag; + target.replace_contents(code.data, null, false, FileCreateFlags.REPLACE_DESTINATION, out etag); + } catch (Error error) { + warning("Failed to save code: %s", error.message); + } + } + } + dialog.destroy(); + }); + dialog.show(); + } + + public override void dispose() { + if (realize_id != 0) { + disconnect(realize_id); + realize_id = 0; + } + base.dispose(); + } +} + +} diff --git a/main/src/ui/conversation_content_view/message_widget.vala b/main/src/ui/conversation_content_view/message_widget.vala index 25d3775b..f8db1a45 100644 --- a/main/src/ui/conversation_content_view/message_widget.vala +++ b/main/src/ui/conversation_content_view/message_widget.vala @@ -30,6 +30,7 @@ public class MessageMetaItem : ContentMetaItem { uint pending_timeout_id = -1; public Label label = new Label("") { use_markup=true, xalign=0, selectable=true, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, hexpand=true, vexpand=true }; + private Widget? content_widget; public MessageMetaItem(ContentItem content_item, StreamInteractor stream_interactor) { base(content_item); @@ -73,111 +74,159 @@ public class MessageMetaItem : ContentMetaItem { update_label(); } - private void generate_markup_text(ContentItem item, Label label) { - MessageItem message_item = item as MessageItem; - Conversation conversation = message_item.conversation; + private void generate_markup_text(Label label, string text, ArrayList markups, int action_name_length) { Message message = message_item.message; + label.set_attributes(create_text_attributes(text, 0, markups, action_name_length)); - // Get a copy of the markup spans, such that we can modify them - var markups = new ArrayList(); - foreach (var markup in message.get_markups()) { - markups.add(new Xep.MessageMarkup.Span() { types=markup.types, start_char=markup.start_char, end_char=markup.end_char }); + bool theme_dependent; + string markup_text = parse_text_markup(label, text, out theme_dependent); + + int only_emoji_count = Util.get_only_emoji_count(markup_text); + if (only_emoji_count != -1) { + string size_str = only_emoji_count < 5 ? "xx-large" : "large"; + markup_text = @"" + markup_text + ""; } - string markup_text = message.body; + string dim_color = Util.is_dark_theme(label) ? "#BDBDBD" : "#707070"; - var attrs = new AttrList(); - label.set_attributes(attrs); + if (message.body == "") { + markup_text = @"%s".printf(_("Message deleted")); + theme_dependent = true; + } + markup_text += get_meta_markup(label, true, ref theme_dependent); - if (markup_text == null) return; // TODO remove + if (theme_dependent && realize_id == -1 && !label.get_realized()) { + realize_id = label.realize.connect(update_label); + } else if (!theme_dependent && realize_id != -1) { + label.disconnect(realize_id); + } + label.label = markup_text; + } - // Only process messages up to a certain size - if (markup_text.length > 10000) { - markup_text = markup_text.substring(0, 10000) + " [" + _("Message too long") + "]"; + private ArrayList copy_markups(Message message) { + var markups = new ArrayList(); + foreach (var markup in message.get_markups()) { + markups.add(new Xep.MessageMarkup.Span() { types=markup.types, start_char=markup.start_char, end_char=markup.end_char }); } + return markups; + } - bool theme_dependent = false; + private string prepare_message(ArrayList markups, out int action_name_length) { + Message message = message_item.message; + string text = message.body ?? ""; + action_name_length = 0; - markup_text = Util.remove_fallbacks_adjust_markups(markup_text, message.quoted_item_id > 0, message.get_fallbacks(), markups); + if (text.char_count() > 10000) { + text = text.substring(0, text.index_of_nth_char(10000)) + " [" + _("Message too long") + "]"; + } - var bold_attr = Pango.attr_weight_new(Pango.Weight.BOLD); - var italic_attr = Pango.attr_style_new(Pango.Style.ITALIC); - var strikethrough_attr = Pango.attr_strikethrough_new(true); + text = Util.remove_fallbacks_adjust_markups(text, message.quoted_item_id > 0, message.get_fallbacks(), markups); - // Prefix message with name instead of /me - if (markup_text.has_prefix("/me ")) { - string display_name = Util.get_participant_display_name(stream_interactor, conversation, message.from); - markup_text = display_name + " " + markup_text.substring(4); + if (text.has_prefix("/me ")) { + string display_name = Util.get_participant_display_name(stream_interactor, message_item.conversation, message.from); + text = display_name + " " + text.substring(4); + action_name_length = display_name.char_count(); foreach (Xep.MessageMarkup.Span span in markups) { - int length = display_name.char_count() - 4 + 1; - span.start_char += length; - span.end_char += length; + int difference = action_name_length - 3; + span.start_char += difference; + span.end_char += difference; } - - bold_attr.end_index = display_name.length; - italic_attr.end_index = display_name.length; - attrs.insert(bold_attr.copy()); - attrs.insert(italic_attr.copy()); } + return text; + } + + private Label create_text_label() { + var text_label = new Label("") { + use_markup=true, + xalign=0, + selectable=true, + wrap=true, + wrap_mode=Pango.WrapMode.WORD_CHAR, + hexpand=true + }; + text_label.activate_link.connect(on_label_activate_link); + return text_label; + } + + private void add_text_attribute(AttrList attributes, Attribute attribute, string text, int source_start, int span_start, int span_end) { + int source_end = source_start + text.char_count(); + int start = int.max(source_start, span_start); + int end = int.min(source_end, span_end); + if (start >= end) return; + + attribute.start_index = text.index_of_nth_char(start - source_start); + attribute.end_index = text.index_of_nth_char(end - source_start); + attributes.insert(attribute.copy()); + } + + private AttrList create_text_attributes(string text, int source_start, ArrayList markups, int action_name_length) { + var attributes = new AttrList(); foreach (var markup in markups) { - foreach (var ty in markup.types) { - Attribute attr = null; - switch (ty) { + foreach (var type in markup.types) { + Attribute attribute; + switch (type) { case Xep.MessageMarkup.SpanType.EMPHASIS: - attr = Pango.attr_style_new(Pango.Style.ITALIC); + attribute = Pango.attr_style_new(Pango.Style.ITALIC); break; case Xep.MessageMarkup.SpanType.STRONG_EMPHASIS: - attr = Pango.attr_weight_new(Pango.Weight.BOLD); + attribute = Pango.attr_weight_new(Pango.Weight.BOLD); break; case Xep.MessageMarkup.SpanType.DELETED: - attr = Pango.attr_strikethrough_new(true); + attribute = Pango.attr_strikethrough_new(true); break; + default: + continue; } - attr.start_index = markup_text.index_of_nth_char(markup.start_char); - attr.end_index = markup_text.index_of_nth_char(markup.end_char); - attrs.insert(attr.copy()); + add_text_attribute(attributes, (owned) attribute, text, source_start, markup.start_char, markup.end_char); } } - // Work around pango bug - markup_text = Util.unbreak_space_around_non_spacing_mark((owned) markup_text); + if (action_name_length > 0) { + add_text_attribute(attributes, Pango.attr_weight_new(Pango.Weight.BOLD), text, source_start, 0, action_name_length); + add_text_attribute(attributes, Pango.attr_style_new(Pango.Style.ITALIC), text, source_start, 0, action_name_length); + } + + return attributes; + } + private string parse_text_markup(Label text_label, string text, out bool theme_dependent) { + string markup_text = Util.unbreak_space_around_non_spacing_mark(text); + theme_dependent = false; + Conversation conversation = message_item.conversation; if (conversation.type_ == Conversation.Type.GROUPCHAT) { - markup_text = Util.parse_add_markup_theme(markup_text, conversation.nickname, true, true, true, Util.is_dark_theme(this.label), ref theme_dependent); - } else { - markup_text = Util.parse_add_markup_theme(markup_text, null, true, true, true, Util.is_dark_theme(this.label), ref theme_dependent); + return Util.parse_add_markup_theme(markup_text, conversation.nickname, true, true, true, Util.is_dark_theme(text_label), ref theme_dependent); } + return Util.parse_add_markup_theme(markup_text, null, true, true, true, Util.is_dark_theme(text_label), ref theme_dependent); + } - int only_emoji_count = Util.get_only_emoji_count(markup_text); - if (only_emoji_count != -1) { - string size_str = only_emoji_count < 5 ? "xx-large" : "large"; - markup_text = @"" + markup_text + ""; - } + private void format_text_segment(Label text_label, CodeBlockPart part, ArrayList markups, int action_name_length) { + text_label.set_attributes(create_text_attributes(part.text, part.source_start_char, markups, action_name_length)); + bool theme_dependent; + text_label.label = parse_text_markup(text_label, part.text, out theme_dependent); + } - string dim_color = Util.is_dark_theme(this.label) ? "#BDBDBD" : "#707070"; + private string get_meta_markup(Label meta_label, bool inline, ref bool theme_dependent) { + Message message = message_item.message; + string dim_color = Util.is_dark_theme(meta_label) ? "#BDBDBD" : "#707070"; + string separator = inline ? " " : ""; + string markup = ""; - if (message.body == "") { - markup_text = @"%s".printf(_("Message deleted")); - theme_dependent = true; - } if (message.edit_to != null) { - markup_text += @" (%s)".printf(_("edited")); + markup = @"$separator(%s)".printf(_("edited")); + separator = " "; theme_dependent = true; } - // Append message status info additional_info = AdditionalInfo.NONE; if (message.direction == Message.DIRECTION_SENT && (message.marked == Message.Marked.SENDING || message.marked == Message.Marked.UNSENT)) { - // Append "pending..." iff message has not been sent yet if (message.time.compare(new DateTime.now_utc().add_seconds(-10)) < 0) { - markup_text += @" %s".printf(_("pending…")); + markup += @"$separator%s".printf(_("pending…")); theme_dependent = true; additional_info = AdditionalInfo.PENDING; } else { int time_diff = (- (int) message.time.difference(new DateTime.now_utc()) / 1000); - if (pending_timeout_id != -1) Source.remove(pending_timeout_id); pending_timeout_id = Timeout.add(10000 - time_diff, () => { update_label(); pending_timeout_id = -1; @@ -185,24 +234,62 @@ public class MessageMetaItem : ContentMetaItem { }); } } else if (message.direction == Message.DIRECTION_SENT && message.marked == Message.Marked.ERROR) { - // Append "delivery failed" if there was a server error - // TODO: Use a dedicated label, not appended text - string error_color = Util.rgba_to_hex(Util.get_label_pango_color(label, "error_color")); - markup_text += " %s".printf(error_color, _("delivery failed")); + string error_color = Util.rgba_to_hex(Util.get_label_pango_color(meta_label, "error_color")); + markup += @"$separator%s".printf(_("delivery failed")); theme_dependent = true; additional_info = AdditionalInfo.DELIVERY_FAILED; } - if (theme_dependent && realize_id == -1) { - realize_id = label.realize.connect(update_label); - } else if (!theme_dependent && realize_id != -1) { - label.disconnect(realize_id); + return markup; + } + + private Widget generate_code_widget(ArrayList parts, ArrayList markups, int action_name_length) { + var content = new Box(Orientation.VERTICAL, 6) { hexpand=true }; + foreach (CodeBlockPart part in parts) { + if (part.is_code) { + content.append(new CodeBlockWidget(part.text, part.language)); + } else { + Label text_label = create_text_label(); + format_text_segment(text_label, part, markups, action_name_length); + content.append(text_label); + } } - label.label = markup_text; + + Label meta_label = create_text_label(); + bool theme_dependent = false; + string meta_markup = get_meta_markup(meta_label, false, ref theme_dependent); + if (meta_markup != "") { + meta_label.label = meta_markup; + content.append(meta_label); + } + return content; } public void update_label() { - generate_markup_text(content_item, label); + if (pending_timeout_id != -1) { + Source.remove(pending_timeout_id); + pending_timeout_id = -1; + } + if (realize_id != -1) { + label.disconnect(realize_id); + realize_id = -1; + } + + var markups = copy_markups(message_item.message); + int action_name_length; + string text = prepare_message(markups, out action_name_length); + bool has_blocks; + ArrayList parts = CodeBlockParser.parse(text, out has_blocks); + if (has_blocks) { + content_widget = generate_code_widget(parts, markups, action_name_length); + } else { + generate_markup_text(label, text, markups, action_name_length); + content_widget = label; + } + + if (outer != null && !in_edit_mode) { + outer.set_widget(content_widget, Plugins.WidgetType.GTK4, 2); + } } public override Object? get_widget(Plugins.ConversationItemWidgetInterface outer, Plugins.WidgetType type) { @@ -210,7 +297,7 @@ public class MessageMetaItem : ContentMetaItem { this.notify["in-edit-mode"].connect(on_in_edit_mode_changed); - outer.set_widget(label, Plugins.WidgetType.GTK4, 2); + outer.set_widget(content_widget, Plugins.WidgetType.GTK4, 2); if (message_item.message.quoted_item_id > 0) { var quoted_content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(message_item.conversation, message_item.message.quoted_item_id); @@ -223,7 +310,7 @@ public class MessageMetaItem : ContentMetaItem { outer.set_widget(quote_widget, Plugins.WidgetType.GTK4, 1); } } - return label; + return content_widget; } public override Gee.List? get_item_actions(Plugins.WidgetType type) { @@ -267,7 +354,7 @@ public class MessageMetaItem : ContentMetaItem { edit_mode.cancelled.connect(() => { in_edit_mode = false; - outer.set_widget(label, Plugins.WidgetType.GTK4, 2); + outer.set_widget(content_widget, Plugins.WidgetType.GTK4, 2); }); edit_mode.send.connect(() => { string text = edit_mode.chat_text_view.text_view.buffer.text; @@ -275,7 +362,7 @@ public class MessageMetaItem : ContentMetaItem { Dino.send_message(message_item.conversation, text, message_item.message.quoted_item_id, message_item.message, markups); in_edit_mode = false; - outer.set_widget(label, Plugins.WidgetType.GTK4, 2); + outer.set_widget(content_widget, Plugins.WidgetType.GTK4, 2); }); edit_mode.chat_text_view.set_text(message); diff --git a/main/src/ui/conversation_content_view/syntax_highlighting.vala b/main/src/ui/conversation_content_view/syntax_highlighting.vala new file mode 100644 index 00000000..b45ba81c --- /dev/null +++ b/main/src/ui/conversation_content_view/syntax_highlighting.vala @@ -0,0 +1,136 @@ +using Gee; + +namespace Dino.Ui.CodeSyntax { + +public enum TokenStyle { + COMMENT, + CONSTANT, + FUNCTION, + KEYWORD, + NUMBER, + PREPROCESSOR, + STRING, + TYPE +} + +public class HighlightSpan : Object { + public int start; + public int end; + public TokenStyle style; + + public HighlightSpan(int start, int end, TokenStyle style) { + this.start = start; + this.end = end; + this.style = style; + } +} + +public class HighlightRule : Object { + public TokenStyle style; + private Regex regex; + + public HighlightRule(TokenStyle style, string pattern) { + this.style = style; + try { + regex = new Regex(pattern, RegexCompileFlags.OPTIMIZE); + } catch (RegexError error) { + GLib.error("Invalid syntax highlighting rule: %s", error.message); + } + } + + public bool find(string text, int offset, out int start, out int end) { + MatchInfo match_info; + try { + regex.match_full(text, -1, offset, 0, out match_info); + } catch (RegexError error) { + start = -1; + end = -1; + return false; + } + if (!match_info.matches()) { + start = -1; + end = -1; + return false; + } + match_info.fetch_pos(0, out start, out end); + return true; + } +} + +public class LanguageDefinition : Object { + public string id; + public string name; + public string extension; + public string[] aliases; + public HighlightRule[] rules; + + public LanguageDefinition(string id, string name, string extension, string[] aliases, HighlightRule[] rules) { + this.id = id; + this.name = name; + this.extension = extension; + this.aliases = aliases; + this.rules = rules; + } + + public bool matches(string language) { + string normalized = language.down().strip(); + if (normalized == id) return true; + foreach (string alias in aliases) { + if (normalized == alias) return true; + } + return false; + } + + public ArrayList highlight(string text) { + var spans = new ArrayList(); + int offset = 0; + while (offset < text.length) { + HighlightRule? selected_rule = null; + int selected_start = text.length; + int selected_end = text.length; + foreach (HighlightRule rule in rules) { + int start; + int end; + if (rule.find(text, offset, out start, out end) && start < selected_start) { + selected_rule = rule; + selected_start = start; + selected_end = end; + } + } + if (selected_rule == null || selected_end <= selected_start) break; + spans.add(new HighlightSpan(selected_start, selected_end, selected_rule.style)); + offset = selected_end; + } + return spans; + } +} + +private static LanguageDefinition[]? definitions; + +public static LanguageDefinition? find(string language) { + if (definitions == null) definitions = create_definitions(); + foreach (LanguageDefinition definition in definitions) { + if (definition.matches(language)) return definition; + } + return null; +} + +private static LanguageDefinition[] create_definitions() { + return { create_c_definition() }; +} + +private static LanguageDefinition create_c_definition() { + HighlightRule[] rules = { + new HighlightRule(TokenStyle.COMMENT, "//[^\\r\\n]*|/\\*[\\s\\S]*?\\*/"), + new HighlightRule(TokenStyle.STRING, "\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*'"), + new HighlightRule(TokenStyle.PREPROCESSOR, "(?m)^[\\t ]*#[\\t ]*[A-Za-z_][A-Za-z0-9_]*"), + new HighlightRule(TokenStyle.KEYWORD, "\\b(?:alignas|alignof|asm|auto|break|case|const|constexpr|continue|default|do|else|enum|extern|for|goto|if|inline|register|restrict|return|sizeof|static|static_assert|struct|switch|thread_local|typedef|typeof|typeof_unqual|union|volatile|while|_Alignas|_Alignof|_Atomic|_Generic|_Noreturn|_Static_assert|_Thread_local)\\b"), + new HighlightRule(TokenStyle.TYPE, "\\b(?:bool|char|char8_t|double|float|int|long|short|signed|unsigned|void|_Bool|_Complex|_Decimal32|_Decimal64|_Decimal128|_Imaginary|int8_t|int16_t|int32_t|int64_t|uint8_t|uint16_t|uint32_t|uint64_t|intptr_t|uintptr_t|ptrdiff_t|size_t|ssize_t|wchar_t)\\b"), + new HighlightRule(TokenStyle.CONSTANT, "\\b(?:false|NULL|nullptr|true)\\b"), + new HighlightRule(TokenStyle.NUMBER, "\\b(?:0[xX][0-9A-Fa-f]+|0[bB][01]+|(?:[0-9]+\\.[0-9]*|[0-9]+)(?:[eEpP][+-]?[0-9]+)?)[fFlLuU]*\\b"), + new HighlightRule(TokenStyle.FUNCTION, "\\b[A-Za-z_][A-Za-z0-9_]*(?=[\\t ]*\\()") + }; + return new LanguageDefinition("c", "C", "c", { "h", "c89", "c90", "c99", "c11", "c17", "c18", "c23" }, rules); +} + +} -- cgit v1.3