
Подсветка синтаксиса Visual Basic средствами VB.NET и регулярных выражений
Чтобы показать VB-код на сайте с цветными ключевыми словами, строками и комментариями, не нужна сторонняя библиотека. Достаточно одного регулярного выражения с именованными группами и аккуратного вывода в HTML.
Задача: цветной код в статьях сайта
Сайт с примерами кода почти на каждой странице показывает фрагменты Visual Basic. Без подсветки они читаются тяжело: ключевые слова сливаются с идентификаторами, а закомментированные строки не отличаются от рабочих. Хочется, чтобы редактор контента вставлял код между маркерами [code] и [/code], а сервер при выводе страницы сам раскрашивал его.
Первое, что приходит в голову: заменить ключевые слова на теги, затем строки, затем комментарии. Такой подход быстро обрастает исправлениями, потому что элементы пересекаются. Слово Dim внутри строки «Dim is short» не должно стать синим, апостроф внутри строки не начинает комментарий, а кавычка в комментарии не открывает строку.
Почему многопроходная замена ломается
Классическое решение в несколько проходов выглядит так: сначала пометить все ключевые слова, кавычки и комментарии служебными тегами, затем внутри комментариев убрать теги ключевых слов и строк, затем внутри строк убрать теги ключевых слов и комментариев, и только после этого превратить метки в HTML. Работает, но каждая новая конструкция языка требует ещё одной «заплатки».
Есть и неприятный побочный эффект: служебные метки вроде [kw] сами становятся частью текста. Если подсветить исходный код самого подсветчика, его строковые константы с метками будут съедены заменой, и опубликованный пример окажется нерабочим. Именно так в старых статьях появлялись строки вида Replace(«», …) с пустым аргументом.
Одно выражение с именованными группами
Движок регулярных выражений .NET просматривает текст слева направо и в каждой позиции пробует альтернативы по порядку. Если поставить первой альтернативой строку, второй комментарий, а третьей ключевое слово, то в позиции открывающей кавычки будет захвачена вся строка целиком, вместе с апострофами и словами внутри. Апостроф вне строки начнёт комментарий до конца строки. Ключевые слова будут найдены только в оставшемся тексте.
Именованные группы (?<str>…), (?<com>…) и (?<kw>…) позволяют после совпадения узнать, что именно найдено. Строка VB описывается как кавычка, затем любые символы кроме кавычки и перевода строки или удвоенная кавычка, затем закрывающая кавычка. Комментарий начинается с апострофа или слова REM. Ключевые слова собираются из списка через Regex.Escape и окружаются границами слова \b, чтобы Dim не подсвечивался внутри DimensionCount.
Класс VbHighlighter
Вывод строится вручную: текст между совпадениями кодируется через HtmlEncode и добавляется как есть, а совпадение оборачивается в span с классом. Кодировать обязательно, иначе знаки меньше и больше в выражениях вроде If a < b сломают разметку страницы, а вставленный злоумышленником код превратится в XSS.
- в .NET 2.0 и 3.5 вместо WebUtility используйте HttpUtility.HtmlEncode из System.Web;
- RegexOptions.Compiled ускоряет многократное применение, поэтому экземпляр класса создавайте один раз и храните в статическом поле;
- IgnoreCase нужен потому, что VB не различает регистр: dim и DIM тоже ключевые слова.
Imports System.Linq
Imports System.Net
Imports System.Text
Imports System.Text.RegularExpressions
Public Class VbHighlighter
Private ReadOnly _rx As Regex
Public Sub New(keywords As IEnumerable(Of String))
' ключевые слова экранируем и объединяем через |
Dim kw = String.Join("|", keywords.Select(Function(k) Regex.Escape(k)).ToArray())
Dim pattern = "(?<str>""(?:[^""\r\n]|"""")*"")" &
"|(?<com>(?:'|\bREM\b)[^\r\n]*)" &
"|(?<kw>\b(?:" & kw & ")\b)"
_rx = New Regex(pattern, RegexOptions.IgnoreCase Or RegexOptions.Compiled)
End Sub
' Возвращает безопасный HTML с раскрашенным кодом
Public Function ToHtml(code As String) As String
Dim sb As New StringBuilder("<pre class=""vb"">")
Dim pos = 0
For Each m As Match In _rx.Matches(code)
sb.Append(WebUtility.HtmlEncode(code.Substring(pos, m.Index - pos)))
Dim css = If(m.Groups("str").Success, "s",
If(m.Groups("com").Success, "c", "k"))
sb.Append("<span class=""" & css & """>")
sb.Append(WebUtility.HtmlEncode(m.Value)).Append("</span>")
pos = m.Index + m.Length
Next
sb.Append(WebUtility.HtmlEncode(code.Substring(pos)))
Return sb.Append("</pre>").ToString()
End Function
End ClassСписок ключевых слов и стили
Ключевые слова удобно держать в текстовом файле, по одному на строку: их можно дополнять без перекомпиляции. В VB.NET около двухсот ключевых слов и операторов, и итоговое выражение получается длинным, но это не страшно. Скомпилированный Regex обрабатывает его быстро, а упорядочить слова от длинных к коротким полезно, чтобы альтернатива не останавливалась на префиксе.
Цвета задаются не тегом font, как в ранних версиях, а CSS-классами. Так оформление меняется в одном месте, а тёмная тема сайта получает свою палитру без правки серверного кода.
/* Подсветка VB-кода */
pre.vb { font: 14px/1.5 Consolas, monospace; padding: 12px;
background: #f7f7f9; overflow-x: auto; }
pre.vb .k { color: #0000ff; } /* ключевые слова */
pre.vb .s { color: #a31515; } /* строки */
pre.vb .c { color: #008000; font-style: italic; } /* комментарии */Поиск блоков [code] в тексте статьи
Статья хранится в базе как HTML, в который редактор вставил маркеры. Блоки находятся выражением \[code\](.*?)\[/code\] с опцией Singleline, чтобы точка захватывала переводы строк, и ленивым квантификатором, чтобы два блока на странице не слились в один. Для замены используйте Regex.Replace с MatchEvaluator: в нём вызывается ToHtml для содержимого группы.
Важная деталь: если редактор контента уже закодировал текст (например, превратил кавычки в "), его нужно сначала декодировать через HtmlDecode, а затем раскрасить. Иначе выражение для строк не найдёт ни одной кавычки.
Ограничения подхода
Регулярное выражение не понимает грамматику языка. Оно не отличит имя метода Select от ключевого слова Select, если они пишутся одинаково, не подсветит типы и не разберёт XML-литералы VB 9. Для статей с примерами этого достаточно, но для полноценного редактора кода нужен настоящий лексер.
Многострочные комментарии в VB не существуют, поэтому выражению хватает одной строки. Для C# или JavaScript придётся добавить альтернативу для блоков /* … */ и шаблонных строк, но принцип одного прохода останется тем же.
Как это делают сегодня
Вопросы и ответы
Почему ключевые слова подсвечиваются внутри строк?
Скорее всего, вы делаете несколько отдельных замен. Объедините правила в одно выражение и поставьте альтернативу для строк первой: тогда строка захватывается целиком раньше, чем до её содержимого дойдёт поиск слов.
Медленно ли регулярное выражение с двумя сотнями слов?
С опцией Compiled и одним экземпляром Regex на всё приложение страница с несколькими блоками кода обрабатывается за миллисекунды. Результат можно дополнительно кэшировать вместе с HTML статьи.
Можно ли так подсвечивать C#?
Да, меняются только правила: строки с обратной косой чертой и verbatim-строки, комментарии // и /* */, регистрозависимые ключевые слова без IgnoreCase.