Привет, читатель! В этом посте я расскажу, как можно сделать свой код на Хабре более «живым» благодаря простому способу сохранения его оригинальной цветовой схемы. А также предлагаю сравнить различные варианты расцветки кода.
Предисловие
Сейчас я нахожусь в процессе написания своей первой статьи для Хабра про веб-сервисы. И с самого начала решил, что исходные коды буду подсвечивать сторонним хабра-хайлайтером, так как стандартный тег <source> по моему мнению не дает качественного эффекта подсветки синтаксиса. Однако я обнаружил только один альтернативный хайлайтер — http://highlight.hohli.com. Результат его расцветки меня, к сожалению, не воодушевил. В конце поста я его показываю в числе 6 вариантов расцветки для непосредственного сравнения. Когда-то раньше был еще один хайлатер — Source Code Highlighter, описанный на Хабре тут и тут, но он уже давно не работает. Тут я вспомнил, что код из Visual Studio полностью сохраняет свой вид при копировании в Word, этот нехитрый прием и стал основой моего метода.
Метод
Идея проста: код копируется в Word, документ сохранятся как html веб-страница и функция преобразует html код в хабра-совместимый. Рассмотрим на примере. Этот простой код при сохранении дает следующее содержимое <body>, где синим цветом я обозначил все теги <span>, розовым – цвет стиля, а коричневым – сам отображаемый текст.
private void Hello(){
MessageBox.Show("Hello Habrahabr!");
}
<body lang=RU style='tab-interval:35.4pt'>
<div class=WordSection1>
<p class=MsoNormal style='margin-bottom:0cm;margin-bottom:.0001pt;line-height:
normal;mso-layout-grid-align:none;text-autospace:none'><span lang=EN-US
style='font-size:9.5pt;font-family:Consolas;color:black;background:white;
mso-highlight:white;mso-ansi-language:EN-US'><span
style='mso-spacerun:yes'> </span></span><span class=GramE><span
lang=EN-US style='font-size:9.5pt;font-family:Consolas;color:blue;background:
white;mso-highlight:white;mso-ansi-language:EN-US'>private</span></span><span
lang=EN-US style='font-size:9.5pt;font-family:Consolas;color:black;background:
white;mso-highlight:white;mso-ansi-language:EN-US'> </span><span lang=EN-US
style='font-size:9.5pt;font-family:Consolas;color:blue;background:white;
mso-highlight:white;mso-ansi-language:EN-US'>void</span><span lang=EN-US
style='font-size:9.5pt;font-family:Consolas;color:black;background:white;
mso-highlight:white;mso-ansi-language:EN-US'> Hello(){<o:p></o:p></span></p>
<p class=MsoNormal style='margin-bottom:0cm;margin-bottom:.0001pt;line-height:
normal;mso-layout-grid-align:none;text-autospace:none'><span lang=EN-US
style='font-size:9.5pt;font-family:Consolas;color:black;background:white;
mso-highlight:white;mso-ansi-language:EN-US'><span
style='mso-spacerun:yes'> </span></span><span class=GramE><span
lang=EN-US style='font-size:9.5pt;font-family:Consolas;color:#2B91AF;
background:white;mso-highlight:white;mso-ansi-language:EN-US'>MessageBox</span><span
lang=EN-US style='font-size:9.5pt;font-family:Consolas;color:black;background:
white;mso-highlight:white;mso-ansi-language:EN-US'>.Show(</span></span><span
lang=EN-US style='font-size:9.5pt;font-family:Consolas;color:#A31515;
background:white;mso-highlight:white;mso-ansi-language:EN-US'>"Hello
Habrahabr!"</span><span lang=EN-US style='font-size:9.5pt;font-family:
Consolas;color:black;background:white;mso-highlight:white;mso-ansi-language:
EN-US'>);<o:p></o:p></span></p>
<p class=MsoNormal><span lang=EN-US style='font-size:9.5pt;line-height:115%;
font-family:Consolas;color:black;background:white;mso-highlight:white;
mso-ansi-language:EN-US'><span style='mso-spacerun:yes'> </span></span><span
style='font-size:9.5pt;line-height:115%;font-family:Consolas;color:black;
background:white;mso-highlight:white'>}</span></p>
</div>
</body>
Как видим, каждая строка исходного кода преобразуется в тег <p>, внутри которого с помощью вложенных тегов <span> производится форматирование текста, а его цвет хранится в атрибуте style. Становится ясен алгоритм преобразования: для каждого тега <p> из "/html/body/div/p" произвести рекурсивную трансляцию его содержимого в новый html документ, попутно с помощью тега <font> устанавливая цвет текста. Под спойлером приведен полный исходный код получившегося метода, который выглядит в соответствии с моей оригинальной цветовой схемой. Для работы с html использована библиотека Html Agility Pack.
- using System;
- using System.IO;
- using HtmlAgilityPack;
- namespace HabraCode{
- public static class HabraCodeFormatter{
- const char spaceClassic = (char)32;
- const char spaceNbsp = (char)160;
- public static void Format(string fileHtm, bool withLineNumbers = false){
- var info = new FileInfo(fileHtm);
- string destFile = info.DirectoryName + "\" + info.Name.Replace(info.Extension, null) + "_result.txt";
- Format(fileHtm, destFile, withLineNumbers);
- }
- public static void Format(string fileHtm, string fileDest, bool withLineNumbers = false){
- var doc = new HtmlDocument();
- doc.Load(fileHtm);
- var nodes = doc.DocumentNode.SelectNodes("/html/body/div/p");
- int spacesToDelete = getSpacesToDelete(nodes[0]);
- var result = new HtmlDocument();
- var rootNode = result.CreateElement("blockquote");
- result.DocumentNode.AppendChild(rootNode);
- if (withLineNumbers){
- var nodeOl = result.CreateElement("ol");
- rootNode.AppendChild(nodeOl);
- rootNode = nodeOl;
- }
- foreach (var node in nodes){
- if (withLineNumbers){
- var nodeLi = doc.CreateElement("li");
- nodeLi.AppendChildren(node.ChildNodes);
- node.RemoveAllChildren();
- node.AppendChild(nodeLi);
- }
- int refSpaces = spacesToDelete;
- translateNode(node, rootNode, ref refSpaces);
- rootNode.AppendChild(result.CreateTextNode(Environment.NewLine));
- }
- result.Save(fileDest);
- }
- private static int getSpacesToDelete(HtmlNode node){
- for (int index = 0; index < node.InnerText.Length; index++){
- if (!isSpace(node.InnerText[index])){
- return index;
- }
- }
- return 0;
- }
- private static void translateNode(HtmlNode node, HtmlNode destParent, ref int spacesToDelete){
- HtmlNode destNode = destParent;
- switch (node.Name){
- case "o:p":
- case "p":
- break;
- case "span":
- string color = getColor(node);
- if (color != null){
- destNode = destParent.OwnerDocument.CreateElement("font");
- destNode.SetAttributeValue("color", color);
- }
- break;
- case "#text":
- string text = translateText(node.InnerText, ref spacesToDelete);
- destNode = destParent.OwnerDocument.CreateTextNode(text);
- break;
- case "b":
- case "li":
- destNode = destParent.OwnerDocument.CreateElement(node.Name);
- break;
- default:
- throw new InvalidOperationException("Unexpected node.Name " + node.Name);
- }
- if (!ReferenceEquals(destNode, destParent)){
- destParent.ChildNodes.Add(destNode);
- }
- if (node.HasChildNodes){
- foreach (HtmlNode child in node.ChildNodes){
- translateNode(child, destNode, ref spacesToDelete);
- }
- }
- }
- private static string translateText(string text, ref int spaces){
- if (string.IsNullOrEmpty(text)){
- return text;
- }
- text = text.Replace("http://", "http://");
- text = text.Replace("rn", " ");
- if (spaces > 0 && text.Length >= spaces && isSpace(text.Substring(0, spaces))){
- text = text.Remove(0, spaces);
- spaces = 0;
- }
- text = text.Replace(spaceClassic.ToString(), " ");
- text = text.Replace(spaceNbsp.ToString(), " ");
- return text;
- }
- private static bool isSpace(char ch){
- return (ch == spaceClassic || ch == spaceNbsp);
- }
- private static bool isSpace(string text){
- foreach (char ch in text){
- if (!isSpace(ch)){
- return false;
- }
- }
- return true;
- }
- private static string getColor(HtmlNode node){
- var attr = node.Attributes["style"];
- if (attr == null || attr.Value == null){
- return null;
- }
- string[] values = attr.Value.Split(new[] { ';' });
- foreach (string value in values){
- const string prefixColor = "color:";
- string trimmed = value.Trim();
- if (trimmed.StartsWith(prefixColor)){
- return trimmed.Remove(0, prefixColor.Length);
- }
- }
- return null;
- }
- }
- }
Вероятно, многие назовут мою расцветку кода слишком пестрой, и потому неудобной, однако она ощутимо улучшает мой процесс работы с исходным кодом. Это очень простой способ немного повысить свою эффективность. Немного прокомментирую код. Класс содержит две перегрузки метода Format, где обязательным параметром является путь до исходного html файла, а необязательным – флаг добавления номеров строк. Вторая перегрузка позволяет явно указать конечный файл, иначе результат будет сохранен как [исходное_имя]_result.txt. Метод getSpacesToDelete позволяет найти количество начальных пробелов в первой строке, чтобы удалить их во всех последующих строках.
Настройка шрифта и расцветки исходного кода в Visual Studio производится в меню Tools > Options > Environment > Fonts and Colors. Однако изначально в нем вообще отсутствует возможность установить свой цвет для метода или свойства. Но если вы пользуетесь ReSharper’ом, такая возможность появляется. Для этого в меню ReSharper > Options > Code Inspection > Settings необходимо включить пункт Color identifires. И вот тут можно испытать легкий когнитивный диссонанс, потому что методы станут бирюзового цвета – такой цвет имеют классы в традиционной схеме, а классы станут темно-синими — все это можно увидеть ниже, где я сравниваю расцветки.
В моей схеме классы, ключевые слова и другие элементы имеют классический цвет, но к ним добавляются свои цвета для методов и свойств. Для тех, кто хочет ее попробовать, привожу настройку. Сохраните xml под спойлером как файл с расширением .vssettings в кодировке UTF-8 без BOM, например, HabraCode.vssettings, и сделайте импорт настройки через меню Tools > Import and Export Settings > Import... Кодировку можно задать, например, с помощью Notepad++. По умолчанию мастер также попутно сохраняет все ваши текущие настройки – для возможности отката изменений. После импорта шрифт будет сброшен на дефолтный, и если у вас был кастомный, его надо будет указать заново. Также в настройке «Plain Text» я слегка снизил белизну фону, уменьшив с абсолютной до #FAFAFA.
<UserSettings>
<ApplicationIdentity version="10.0"/>
<ToolsOptions>
<ToolsOptionsCategory name="Environment" RegisteredName="Environment"/>
</ToolsOptions>
<Category name="Environment_Group" RegisteredName="Environment_Group">
<Category name="Environment_FontsAndColors" Category="{1EDA5DD4-927A-43a7-810E-7FD247D0DA1D}" Package="{DA9FB551-C724-11d0-AE1F-00A0C90FFFC3}" RegisteredName="Environment_FontsAndColors" PackageName="Visual Studio Environment Package">
<PropertyValue name="Version">2</PropertyValue>
<FontsAndColors Version="2.0">
<Categories>
<Category GUID="{A27B4E24-A735-4D1D-B8E7-9716E1E3D8E0}">
<Items>
<!--Черный цвет-->
<Item Name="Plain Text" Foreground="0x02000000" Background="0x00FAFAFA" BoldFont="No"/>
<Item Name="Literal" Foreground="0x02000000" Background="0x02000000" BoldFont="Yes"/>
<Item Name="Number" Foreground="0x02000000" Background="0x02000000" BoldFont="Yes"/>
<Item Name="ReSharper Namespace Identifier" Foreground="0x02000000" Background="0x02000000" BoldFont="No"/>
<Item Name="ReSharper Mutable Local Variable Identifier" Foreground="0x02000000" Background="0x02000000" BoldFont="No"/>
<!--Строки: кориченвый -->
<Item Name="String" Foreground="0x001515A3" Background="0x02000000" BoldFont="No"/>
<Item Name="String(C# @ Verbatim)" Foreground="0x001515A3" Background="0x02000000" BoldFont="No"/>
<!--Методы: бледно-малиновый -->
<Item Name="ReSharper Extension Method Identifier" Foreground="0x008812C7" Background="0x02000000" BoldFont="No"/>
<Item Name="ReSharper Method Identifier" Foreground="0x008812C7" Background="0x02000000" BoldFont="No"/>
<Item Name="ReSharper Operator Identifier" Foreground="0x008812C7" Background="0x02000000" BoldFont="No"/>
<!--Свойства, поля, события: рыже-коричневый -->
<Item Name="ReSharper Event Identifier" Foreground="0x001256C7" Background="0x02000000" BoldFont="No"/>
<Item Name="ReSharper Field Identifier" Foreground="0x001256C7" Background="0x02000000" BoldFont="No"/>
<!--Типы: бирюзовый -->
<Item Name="ReSharper Class Identifier" Foreground="0x00AC8500" Background="0x02000000" BoldFont="No"/>
<Item Name="ReSharper Delegate Identifier" Foreground="0x00AC8500" Background="0x02000000" BoldFont="No"/>
<Item Name="ReSharper Enum Identifier" Foreground="0x00AC8500" Background="0x02000000" BoldFont="No"/>
<Item Name="ReSharper Static Class Identifier" Foreground="0x00AC8500" Background="0x02000000" BoldFont="No"/>
<Item Name="ReSharper Interface Identifier" Foreground="0x00AC8500" Background="0x02000000" BoldFont="No"/>
<Item Name="ReSharper Struct Identifier" Foreground="0x00AC8500" Background="0x02000000" BoldFont="No"/>
<Item Name="ReSharper Type Parameter Identifier" Foreground="0x00AC8500" Background="0x02000000" BoldFont="No"/>
<!--Комментарии todo и bug. NotImplementedException: красный-->
<Item Name="ReSharper Todo Item" Foreground="0x000000F5" Background="0x02000000" BoldFont="Yes"/>
<Item Name="ReSharper Todo Item Marker on Error Stripe" Foreground="0x000000F5" Background="0x02000000" BoldFont="No"/>
<!--Элемент string.Format: бирюзовый + bold-->
<Item Name="ReSharper Format String Item" Foreground="0x00AC8500" Background="0x02000000" BoldFont="Yes"/>
<!--Константы: кориченвый + bold -->
<Item Name="ReSharper Constant Identifier" Foreground="0x00004080" Background="0x02000000" BoldFont="Yes"/>
</Items>
</Category>
</Categories>
</FontsAndColors>
</Category>
</Category>
</UserSettings>
Сравнение
Теперь я предлагаю сравнить 6 вариантов расцветки кода на примере одного из методов. Нельзя утверждать, что какая-то расцветка в принципе лучшая, потому что, как известно, на вкус и цвет товарищей нет. Но меня интересует коллективное мнение хабра-сообщества.
1. <source lang="cs">
private static string getColor(HtmlNode node){
var attr = node.Attributes["style"];
if (attr == null || attr.Value == null){
return null;
}
string[] values = attr.Value.Split(new [] {';'});
foreach (string value in values){
const string prefixColor = "color:";
string trimmed = value.Trim();
if (trimmed.StartsWith(prefixColor)){
return trimmed.Remove(0, prefixColor.Length);
}
}
return null;
}
2. Черно-белый код (получен через HabraCodeFormatter с отбросом цвета и bold’а)
private static string getColor(HtmlNode node){
var attr = node.Attributes["style"];
if (attr == null || attr.Value == null){
return null;
}
string[] values = attr.Value.Split(new [] {';'});
foreach (string value in values){
const string prefixColor = "color:";
string trimmed = value.Trim();
if (trimmed.StartsWith(prefixColor)){
return trimmed.Remove(0, prefixColor.Length);
}
}
return null;
}
3. http://highlight.hohli.com
private static string getColor(HtmlNode node){
var attr = node.Attributes["style"];
if (attr == null || attr.Value == null){
return null;
}
string[] values = attr.Value.Split(new [] {';'});
foreach (string value in values){
const string prefixColor = "color:";
string trimmed = value.Trim();
if (trimmed.StartsWith(prefixColor)){
return trimmed.Remove(0, prefixColor.Length);
}
}
return null;
}
4. Стандартная расцветка Visual Studio
private static string getColor(HtmlNode node){
var attr = node.Attributes["style"];
if (attr == null || attr.Value == null){
return null;
}
string[] values = attr.Value.Split(new [] {';'});
foreach (string value in values){
const string prefixColor = "color:";
string trimmed = value.Trim();
if (trimmed.StartsWith(prefixColor)){
return trimmed.Remove(0, prefixColor.Length);
}
}
return null;
}
5. Стандартная расцветка Reshaper
private static string getColor(HtmlNode node){
var attr = node.Attributes["style"];
if (attr == null || attr.Value == null){
return null;
}
string[] values = attr.Value.Split(new [] {';'});
foreach (string value in values){
const string prefixColor = "color:";
string trimmed = value.Trim();
if (trimmed.StartsWith(prefixColor)){
return trimmed.Remove(0, prefixColor.Length);
}
}
return null;
}
6. Авторская расцветка
private static string getColor(HtmlNode node){
var attr = node.Attributes["style"];
if (attr == null || attr.Value == null){
return null;
}
string[] values = attr.Value.Split(new [] {';'});
foreach (string value in values){
const string prefixColor = "color:";
string trimmed = value.Trim();
if (trimmed.StartsWith(prefixColor)){
return trimmed.Remove(0, prefixColor.Length);
}
}
return null;
}
Я бы хотел создать опрос, чтобы узнать, какие из этих вариантов расцветки кода устроили бы хабра-сообщество в публикациях, но у меня недостаточно привилегий.
Автор: capslocky