Подсветка синтаксиса — выделение синтаксических конструкций текста с использованием различных цветов, шрифтов и начертаний. Обычно применяется для облегчения чтения исходного текста компьютерных программ, улучшения визуального восприятия.
Wikipedia
По просьбам моих читателей, которые по счастливой случайности совпали с моими намерениями, я решил убрать поле example.detail, в котором хранился текст в формате RTF. А вместо него добавить текстовое поле example.snippet, в котором будут храниться фрагмент кода с комментариями. Но так как я уже успел накидать в базу примеров, то для переноса текста пришлось сделать кнопку и небольшой скрипт.
Конвертор RTF -> TXT
procedure frmExampleView_Button1_OnClick (Sender: TObject; var Cancel: boolean);
var
tmpDataSet:TDataSet;
begin
SQLQuery('select * from example',tmpDataSet);
while not tmpDataSet.EOF do
begin
frmExampleView.redExample.TextRTF := tmpDataSet.FieldByName('detail').asString;
SQLExecute('UPDATE example SET snippet = '''+ escape_special_characters( frmExampleView.redExample.Text )+''' WHERE id = '+tmpDataSet.FieldByName('id').asString);
tmpDataSet.Next;
end;
tmpDataSet.Free;
frmExampleView.redExample.Clear;
ShowMessage('Перенос завершен!');
end;
Code language: Delphi (delphi)
В коде используется тот факт, что на форме просмотра примера уже есть компонент RichEdit, с помощью которого осуществляется конвертация. Если же потребуется создать функцию конвертации, то придется создавать отдельный экземпляр класса TdbRichEdit.
Кнопка frmExampleView.Button1 временная, я её уберу в следующей версии программы.
Форма редактирования
Для редактирования примера будет использовано обычное поле многострочного редактирования TdbMemo, которому активируем отображение вертикальной полосы прокрутки (свойство ScrollBars = ssVertical).

Форма просмотра
Форма просмотра осталась прежней, с использованием компонента RichEdit, у которого отключено отображение панелей инструментов и линейки (свойства Toolbar1.Show = False, Toolbar2.Show = False, Toolbar3.Show = False, Ruler = False).

Подсветку кода я решил реализовать через формат HTML. Мне показалось это более простым, прозрачным и универсальным решением, чем привязываться к возможностям TdbRichEdit или формату RTF.
К сожалению, в MVDB у RichEdit не реализован метод добавления текста в формате HTML, это возможно сделать только через загрузку файла и метод LoadHTML(), поэтому в копилку расширений добавился ещё один маленький скрипт:
procedure RichEdit_SetHTMLText(AEdit:TdbRichEdit; AText:string);
// загрузка HTML в редактор
var
tmpFileName: string;
tmpMemo: TdbMemo;
begin
tmpFileName := GetTempFilename; // получить имя временного файла
tmpMemo := TdbMemo.Create(nil); // создать компонент для записи текстового файла в форматье UTF-8
tmpMemo.Text := AText;
tmpMemo.SaveToFileUTF8(tmpFileName ); // сохранить файл
AEdit.Clear;
AEdit.LoadHTML(tmpFileName); // загрузить файл как HTML
DeleteFile(tmpFileName); // удалить временный файл
end;
Code language: Delphi (delphi)
Поменялся и метод загрузки данных UserApp_LoadExample(): в нем используется описанный выше RichEdit_SetHTMLText(), а также функция конвертации текста в формат HTML с подсветкой синтаксиса CodeHL_TextToHTML().
procedure UserApp_LoadExample;
// загрузка примера из базы
var
tmpData:string;
tmpTableName: string;
tmpTableID: string;
tmpText: string;
begin
frmExampleView.edtCaption.Text := '';
frmExampleView.redExample.Clear;
if ActiveGrid <> nil then
begin
tmpTableName := Grid_GetTableName( ActiveGrid );
if tmpTableName <> '' then
begin
tmpTableID := IntToStr(ActiveGrid.dbItemID);
try
// получить существующий ID
IDExample := SQLExecute('SELECT id_example FROM '+tmpTableName+' WHERE id = '+tmpTableID );
except // а если ID не существует, то будет -1
IDExample := -1;
end;
// загрузить форматированный текст
tmpText := SQLExecute('SELECT snippet FROM example WHERE id = '+IntToStr(IDExample));
RichEdit_SetHTMLText( frmExampleView.redExample, CodeHL_TextToHTML( tmpText ));
// загрузить название
frmExampleView.edtCaption.Text := SQLExecute('SELECT caption FROM example WHERE id = '+IntToStr(IDExample));
end;
end;
end;
Code language: Delphi (delphi)
Конвертор TXT -> HTML
По сути конвертор – это парсер, просматривающий посимвольно исходный текст. При обнаружении конца слова (одного из символов-разделителей) производится проверка на начало линейного режима, а затем поиск слова в массивах ключевых слов и выполнение специальных действий по форматированию текста – добавлению HTML-последовательностей.
Линейный режим
Линейный режим используется при обнаружения комментариев, а также строковых литералов. В линейном режиме отключается анализ разделения текста на слова до момента обнаружения специального символа (или нескольких символов) завершения линейного режима.
В языке ObjectPascal допустимы три вида комментариев и один способ определения строковых литералов.
Объект | Начальный символ | Конечный символ |
---|---|---|
Строковый литерал | ‘ (одинарная кавычка) | ‘ (одинарная кавычка) |
Комментарий | // | конец строки ($0A) |
Комментарий | { | } |
Комментарий | (* | *) |
Ключевые слова
В языке ObjectPascal выделают две группы ключевых слов: ключевые слова языка и стандартные классы. Каждая группа имеет свой цвет и стиль выделения. Кроме ключевых слов обычно выделяются числовые значения. Весь описанный функционал я реализовал в процедуре CodeHL_CheckWord().
procedure CodeHL_CheckWord(var AWord:string );
// проверка слова, добавление тегов
var
s:string;
i: integer;
j: integer;
begin
s := LowerCase(Trim(AWord));
if (s = '') or (s = ',') then
exit;
// если слово число
if ValidInt(s) or ValidFloat(s) then
begin
AWord := '<font color="#FF00FF"><b>'+AWord+'</b></font>';
exit;
end;
// если слово входит в список 1
for i:=0 to Length(CHL_KeyWord1) - 1 do
begin
j := CompareText(s,CHL_KeyWord1[i]);
if j = 0 then
begin
AWord := '<font color="#005F00"><b>'+AWord+'</b></font>';
exit;
end
else
if j = -1 then
begin
break
end;
end;
// если слово входит в список 2
for i:=0 to Length(CHL_KeyWord2) - 1 do
begin
j := CompareText(s,CHL_KeyWord2[i]);
if j = 0 then
begin
AWord := '<font color="#000000"><b>'+AWord+'</b></font>';
exit;
end
else
if j = -1 then
begin
break
end;
end;
end;
Code language: Delphi (delphi)
Основная функция подсветки кода CodeHL_TextToHTML() кроме описанных выше действий производит замену символов, используемых для разметки, на специальные символьные последовательности. А весь текст оборачивается тегами, которые сохраняют пробелы и переносы строк при отображении данных.
function CodeHL_TextToHTML (AText: string):string;
// добавление в текст HTML тегов с подсветкой
var
i: integer;
j: integer;
tmpWord: string;
tmpChar: string;
s1, s2: string;
tmpIndex: integer;
begin
tmpIndex := -1;
Result := '';
tmpWord := '';
for i := 1 to Length(AText) do
begin
tmpChar := AText[i];
// замена для вставки в HTML
case tmpChar of
'<': tmpChar:='<';
'>': tmpChar:='>';
'&': tmpChar:='&';
chr(10): tmpChar:='';
end;
// линейный режим - это отключение посимвольного анализа до обнаружения триггерной последовательности
if tmpIndex <> -1 then
begin
s1 := CHL_LM_End[tmpIndex];
// обнаружена завершающая последовательность?
if copy(AText,i,Length(s1) ) = s1 then
begin
tmpWord := tmpWord + S1;
// если завершающая последовательность больше одного символа, то делаем коректировку
if Length(S1) > 1 then
delete( AText, i+1, Length(s1) - 1 );
//
if CHL_LM_Italic[tmpIndex] = '1' then
begin
s1 := '<i>';
s2 := '</i>';
end
else
begin
s1 := '';
s2 := '';
end;
Result := Result + '<font color="'+CHL_LM_Color[tmpIndex]+'">'+S1+ tmpWord +S2+ '</font>';
tmpIndex := -1;
tmpWord := '';
end
else
tmpWord := tmpWord + tmpChar;
continue;
end;
// конец слова
if AText[i] in [' ','{','}','/','\',')','(',';',':','"','''','.',chr(13)] then
begin
// проверка начала линейного режима
for j:=0 to Length( CHL_LM_Begin )-1 do
begin
if copy( AText, i, Length(CHL_LM_Begin[j]) ) = CHL_LM_Begin[j] then
begin
CodeHL_CheckWord(tmpWord);
Result := Result + tmpWord;
tmpIndex := j;
continue;
end;
end;
// проверка слов
CodeHL_CheckWord(tmpWord);
Result := Result + tmpWord;
Result := Result + AText[i]; // символ-разделитель, который был в конце слова
tmpWord := '';
end
else
tmpWord := tmpWord + tmpChar;
end;
Result := Result + tmpWord;
Result := '<pre>'+Result+'</pre>';
end;
Code language: Delphi (delphi)
И хотя местами функция выглядит шероховатой, в целом она справляется со своей задачей и имеет приемлемую производительность: задержка при отображении данных составляет около половины секунды, включая создание временного файла для загрузки HTML.
Данные
Все необходимые для работы данные хранятся в массивах, которые заполняются процедурой CodeHL_Init(). В дальнейшем это можно использовать для расширения функциональности, добавляя различные языки для подсветки. В рамках описываемой в данной статье программы точно понадобится язык SQL, так как часть задач касается именно использованию SQL запросов и команд, и было бы неплохо подсвечивать синтаксис этого языка.
var
CHL_KeyWord1 : array of string; // ключевые слова языка
CHL_KeyWord2 : array of string; // стандартные типы
// параметры линейного режима
CHL_LM_Begin: array of string; // начальный символ
CHL_LM_End: array of string; // конечный символ
CHL_LM_Color: array of string; // цвет как код
CHL_LM_Italic: array of string; // признак наклонности текста
procedure CodeHL_Init;
// инициализация
// слова должны быть упорядочены по алфавиту
begin
// настраиваем данные для линейного режима
CHL_LM_Begin := SplitString('//,{,(*,''',',');
CHL_LM_End := SplitString(chr(13)+',},*),''',',');
CHL_LM_Color := SplitString('#00009F,#00009F,#00009F,#FF0000',',');
CHL_LM_Italic := SplitString('0,0,0,1',',');
// данные по ключевым словам
CHL_KeyWord1 := SplitString('absolute,abstract,and,array,as,asm,assembler,automated,begin,case,'+
'cdecl,class,const,constructor,destructor,dispid,dispinterface,div,do,downto,dynamic,'+
'else,end,except,export,exports,external,far,file,finalization,finally,for,forward,'+
'function,goto,if,implementation,in,inherited,initialization,inline,interface,is,'+
'label,library,message,mod,near,nil,not,object,of,or,out,override,packed,pascal,'+
'private,procedure,program,property,protected,public,published,raise,record,register,'+
'repeat,resourcestring,safecall,set,shl,shr,stdcall,then,threadvar,to,try,type,unit,'+
'until,uses,var,virtual,while,with,xor',',');
CHL_KeyWord2 := SplitString('ansichar,ansistring,array,boolean,byte,bytebool,cardinal,'+
'char,currency,double,false,fixedint,fixeduint,int64,integer,longbool,longint,'+
'longword,nativeint,nativeuint,pansichar,pansistring,pboolean,pbyte,pcardinal,'+
'pchar,pcurrency,pdouble,pfixedint,pfixeduint,pint64,pinteger,plongbool,plongint,'+
'plongword,pnativeint,pnativeuint,pointer,ppointer,prawbytestring,pshortint,'+
'pshortstring,psingle,psmallint,pstring,puint64,punicodestring,pvariant,'+
'pwidechar,pwidestring,pword,pwordbool,rawbytestring,real,record,shortint,'+
'shortstring,single,smallint,string,tdoublerec,text,true,tsinglerec,type,'+
'uint64,unicodestring,variant,widechar,widestring,word,wordbool',',' );
end;
Code language: PHP (php)
Список стандартных типов включает все типы, которые доступны в Delphi, но в My Visual Database их гораздо меньше. Если сократить данный список, оставив только необходимые значения, то производительность модуля подсветки будет ещё выше.

Что дальше?
Внимательный читатель заметил, что на картинке с формой редактирования присутствует кнопка “Форматировать код”, действие которой заслуживает отдельного описания. Эта кнопка позволяет выравнивать отступы, переносить строчки и выполнять другие действия, в результате которых исходный код будет выглядеть лучше. Но об этом я расскажу в следующей статье.