На свете существует много разных полезных редакторов, которые помогают авторам статей готовить контент к публикации на веб-ресурсах. Но к сожалению, очень часто бывает что лучшее – враг хорошего, и приходится пользоваться не очень удобными программами которые вместо того чтобы взять и заточить «свой» редактор под нужные цели. Мой подход к этому вопросу как раз заключается в написании своего редактора. В этом посте, я хочу рассказать про то, как и какие фичи я реализовал.
Если взять тот же Microsoft Word, то там есть например возможность добавить содержание (table of contents) или ссылке (references/endnotes/footnotes) в текст. Для публикации на веб-ресурсах эти фичи порой нужны, хотя не все порталы поддерживают, например, именованые ссылки. Но тем не менее, две фичи, которые мне удалось добавить в свой редактор – это автогенерация TOC и списка ссылок («заметок»).
Список элементов содержания показан чуть выше в этом посте. Его суть – позволить быструю навигацию по элементам поста или статьи. Естественно, этот список автогенерируется используя тэги
Содержание полезно в основном для больших постов, например если вы пишете статью на CodeProject. Кстати замечу, что если заголовков нет, то и содержание не создается. А уровень заголовка содержания (если оно есть) всегда соответствует верхнему уровню заголовка в файле.
Реализация такого списка была непростой задачей. Вот кусочек кода, который иллюстрирует как это сделано:
Заметки, или ссылки, это дополнительные, незначительные мысли, которые фигурируют после самой статьи. Я предпочитаю добавлять их в код в квадратных скобках,
К сожалению, заметки не вытащить regexp’ами – ведь у вас могу быть просто квадратные скобки, например в секции
Иногда нужно продемонстрировать какой-то шрифт, иногда хочется что-то написать что не будет поисковиком индексировано, иногда просто хочется защитить свой контент от плагиата. Поэтому, иметь возможность вывести текст как графику – это полезно. А иметь возможность вывести его используя ClearType и OpenType – вообще супер. Поэтому для этих целей мне пришлось к своему управляемому (WPF) приложению дописать «неуправляемую» часть, которая рисует красивый текст используя Direct2D – новый API для двумерной графики от Microsoft. Как это работает? Ну вот пишу я например
После того как у меня заработал обычный текст, я добавил собственно поддержку шрифтов, размеров и фич OpenType чтобы все рисовалось еще красивее.
Получив возможность безнаказанно подменять текст графикой, я автоматизировал этот процесс так, что например могу заменить все заголовки графикой собственного производства.
Один ценный сниппет, который я наконец-то написал – это метод измерения полезных размеров битмапа после того как на нем нарисован текст. Раньше я делал это в GDI+, но недавно разозлился и написал на С++:
RANT: мне надоело видеть на Хабре статьи где после куска кода написано:
Мне для редактора потребовалась точно такая же функциональность. В результате, я воспользовался той же библиотекой что и заспамивший всех хайлайтер (не помню точно как его там), подпил чуть-чуть АПИ и вот, у меня получилось – можно писать
Графы – это мое новое увлечение. Иногда просто по другому не объяснить мысль. Опять же, автогенерация графика графа это в принципе просто, а на практике привело к созданию DSL которая позволяет мне писать простые команды, а на выходе рисует граф. Из такой вот спеки
Получается такая вот картинка:
Это конечно DSLка, причем реализовал я ее через reflection. Каждая директива
В кусочке кода выше, я постарался закэшировать дорогой поиск инфы о методе. Возможно есть решения и получше, но у меня все работает. Это в очередной раз показывает, что не всегда нужно использовать F# или лезть в DLR чтобы быстро получить интерпретатор маленькой DSL.
Вот собственно все, о чем хотел рассказать.
Содержание
Автогенерация контента
Если взять тот же Microsoft Word, то там есть например возможность добавить содержание (table of contents) или ссылке (references/endnotes/footnotes) в текст. Для публикации на веб-ресурсах эти фичи порой нужны, хотя не все порталы поддерживают, например, именованые ссылки. Но тем не менее, две фичи, которые мне удалось добавить в свой редактор – это автогенерация TOC и списка ссылок («заметок»).
Содержание
Список элементов содержания показан чуть выше в этом посте. Его суть – позволить быструю навигацию по элементам поста или статьи. Естественно, этот список автогенерируется используя тэги
H1
—H8
, которые он находит в документе. Просто выдирает их, находит самый верхний уровень заголовка (например для Хабра это H3
), и делает соответствующие списки с помощью вложенных элементов UL
и LI
. Это не очень безопасно, но для поиска заголовков используется вот такое простенькое выражение:var r = new Regex("<h([^<]+)>(.+)</h.>");<br/>
Содержание полезно в основном для больших постов, например если вы пишете статью на CodeProject. Кстати замечу, что если заголовков нет, то и содержание не создается. А уровень заголовка содержания (если оно есть) всегда соответствует верхнему уровню заголовка в файле.
Реализация такого списка была непростой задачей. Вот кусочек кода, который иллюстрирует как это сделано:
private static string GenerateToc([NotNull]string text, ConversionOptions options, out int minLevel)<br/>
{<br/>
List<HeadingEntry> entries = new List<HeadingEntry>();<br/>
var r = new Regex("<h([^<]+)>(.+)</h.>");<br/>
var matches = r.Matches(text);<br/>
int count = 0;<br/>
foreach (Match m in matches)<br/>
{<br/>
int n;<br/>
if (int.TryParse(m.Groups[1].Value, out n))<br/>
{<br/>
HeadingEntry he = new HeadingEntry(n, m.Groups[2].Value);<br/>
// set tag text if clash
bool bFound = false;<br/>
foreach (HeadingEntry h in entries)<br/>
if (h.SuggestedTagText == he.SuggestedTagText)<br/>
bFound = true;<br/>
he.TagText = he.SuggestedTagText + (bFound ? (count++).ToString() : string.Empty);<br/>
entries.Add(he);<br/>
// replace only first occurence
//text = text.Replace(m.Groups[0].Value,
// string.Format("<h{0}><a name=\"{2}\"></a>{1}</h{0}>", n, he.Text, he.TagText));
int idx = text.IndexOf(m.Groups[0].Value);<br/>
string emptyLink = options.UseImageHeadings ? string.Empty :<br/>
string.Format("<a name=\"{0}\"></a>", he.TagText);<br/>
text = text.Remove(idx, m.Groups[0].Value.Length).Insert(idx,<br/>
string.Format("<h{0}>{2}{1}</h{0}>", n, he.Text, emptyLink));<br/>
}<br/>
}<br/>
minLevel = int.MaxValue;<br/>
if (entries.Count > 0)<br/>
{<br/>
var hb = new HtmlBuilder();<br/>
// all are, essentially, ULs
int lastLevel = -1;<br/>
foreach (HeadingEntry he in entries)<br/>
{<br/>
// if this level > last, open a new UL
if (he.Level > lastLevel)<br/>
hb.AppendLine("<ul>").Indent();<br/>
if (he.Level < lastLevel)<br/>
{<br/>
int diff = lastLevel - he.Level;<br/>
while (--diff >= 0)<br/>
hb.Unindent().AppendLine("</ul>");<br/>
}<br/>
hb.AppendLine(string.Format("<li><a href=\"#{0}\">{1}</a></li>", he.TagText, he.Text));<br/>
minLevel = Math.Min(minLevel, he.Level);<br/>
lastLevel = he.Level;<br/>
}<br/>
// close out all indent levels
while (hb.IndentLevel > 0)<br/>
hb.Unindent().AppendLine("</ul>");<br/>
if (!string.IsNullOrEmpty(options.TocLabel))<br/>
hb.Insert(string.Format("<h{0}>{1}</h{0}>{2}", minLevel,<br/>
HttpUtility.HtmlEncode(options.TocLabel), Environment.NewLine), 0);<br/>
// at this point, hb contains the TOC, so
if (!string.IsNullOrEmpty(options.TocAfterToken))<br/>
{<br/>
int idx = text.IndexOf(options.TocAfterToken);<br/>
if (idx >= 0)<br/>
return text.Substring(0, idx + options.TocAfterToken.Length) +<br/>
hb + text.Substring(idx + options.TocAfterToken.Length);<br/>
}<br/>
hb.Append(text);<br/>
return hb.ToString();<br/>
}<br/>
return text;<br/>
}<br/>
Заметки
Заметки, или ссылки, это дополнительные, незначительные мысли, которые фигурируют после самой статьи. Я предпочитаю добавлять их в код в квадратных скобках,
[вот так]
. Каждой сноске присваивается цифра[1] в порядке встречи в файле, а потом они аккуратно группируются в конце документа.К сожалению, заметки не вытащить regexp’ами – ведь у вас могу быть просто квадратные скобки, например в секции
PRE
. Поэтому «отлов» квадратных скобок реален только в процессе обхода файла (по букве) трансформатором. Да-да, это тот же трансформатор, что делает красивые прочерки и кавычки.Текст как картинки
Иногда нужно продемонстрировать какой-то шрифт, иногда хочется что-то написать что не будет поисковиком индексировано, иногда просто хочется защитить свой контент от плагиата. Поэтому, иметь возможность вывести текст как графику – это полезно. А иметь возможность вывести его используя ClearType и OpenType – вообще супер. Поэтому для этих целей мне пришлось к своему управляемому (WPF) приложению дописать «неуправляемую» часть, которая рисует красивый текст используя Direct2D – новый API для двумерной графики от Microsoft. Как это работает? Ну вот пишу я например
[{Hello, World!}]
а система в ответ на это выдает:После того как у меня заработал обычный текст, я добавил собственно поддержку шрифтов, размеров и фич OpenType чтобы все рисовалось еще красивее.
Получив возможность безнаказанно подменять текст графикой, я автоматизировал этот процесс так, что например могу заменить все заголовки графикой собственного производства.
Один ценный сниппет, который я наконец-то написал – это метод измерения полезных размеров битмапа после того как на нем нарисован текст. Раньше я делал это в GDI+, но недавно разозлился и написал на С++:
MYAPI RECT MeasureCropArea(BYTE* src, int width, int height, int stride, int color)<br/>
{<br/>
Pixel& bgr = *reinterpret_cast<Pixel*>(&color);<br/>
RECT result;<br/>
// find the first non-conforming row of pixels
for (int y = 0; y < height; ++y) <br/>
{<br/>
int y_offset = y * stride;<br/>
for (int x = 0; x < width; ++x)<br/>
{<br/>
int offset = x * sizeof(Pixel) + y_offset;<br/>
Pixel& s = *reinterpret_cast<Pixel*>(src + offset);<br/>
// if this pixel is non-conforming, so is the row
if (s != bgr)<br/>
{<br/>
result.top = y;<br/>
// cause soft eject
x = width;<br/>
y = height;<br/>
}<br/>
}<br/>
}<br/>
// find the last non-conforming row of pixels
for (int y = height - 1; y >= 0; --y) <br/>
{<br/>
int y_offset = y * stride;<br/>
for (int x = 0; x < width; ++x)<br/>
{<br/>
int offset = x * sizeof(Pixel) + y_offset;<br/>
Pixel& s = *reinterpret_cast<Pixel*>(src + offset);<br/>
// if this pixel is non-conforming, so is the row
if (s != bgr)<br/>
{<br/>
result.bottom = y;<br/>
// cause soft eject
x = width;<br/>
y = -1;<br/>
}<br/>
}<br/>
}<br/>
// find the first non-conforming column of pixels
for (int x = 0; x < width; ++x)<br/>
{<br/>
for (int y = 0; y < height; ++y) <br/>
{<br/>
int offset = x * sizeof(Pixel) + y * stride;<br/>
Pixel& s = *reinterpret_cast<Pixel*>(src + offset);<br/>
// if this pixel is non-conforming, so is the column
if (s != bgr)<br/>
{<br/>
result.left = x;<br/>
// cause soft eject
x = width;<br/>
y = height;<br/>
}<br/>
}<br/>
}<br/>
// find the last non-conforming column of pixels
for (int x = width - 1; x >= 0; --x)<br/>
{<br/>
for (int y = 0; y < height; ++y) <br/>
{<br/>
int offset = x * sizeof(Pixel) + y * stride;<br/>
Pixel& s = *reinterpret_cast<Pixel*>(src + offset);<br/>
// if this pixel is non-conforming, so is the column
if (s != bgr)<br/>
{<br/>
result.right = x;<br/>
// cause soft eject
x = -1;<br/>
y = height;<br/>
}<br/>
}<br/>
}<br/>
int w = result.right - result.left;<br/>
int h = result.bottom - result.top;<br/>
if (w < 1) { result.left = 0; result.right = 1; }<br/>
if (h < 1) { result.top = 0; result.bottom = 1; }<br/>
return result;<br/>
}<br/>
Подсветка синтаксиса
RANT: мне надоело видеть на Хабре статьи где после куска кода написано:
This code was highlighted with CodeSyntaxHighlighter
. Это неадекват.Мне для редактора потребовалась точно такая же функциональность. В результате, я воспользовался той же библиотекой что и заспамивший всех хайлайтер (не помню точно как его там), подпил чуть-чуть АПИ и вот, у меня получилось – можно писать
{{ мой код }}
и подсветка будет работать. Примеры кода есть прямо в этом посте :)Графы
Графы – это мое новое увлечение. Иногда просто по другому не объяснить мысль. Опять же, автогенерация графика графа это в принципе просто, а на практике привело к созданию DSL которая позволяет мне писать простые команды, а на выходе рисует граф. Из такой вот спеки
edge basic advanced<br/>
edge advanced TOC`generation<br/>
edge advanced Text-as-image`generation<br/>
edge advanced Graph`generation<br/>
edge Heading`substitution<br/>
edge TOC`generation Heading`substitution<br/>
edge Heading`substitution Migration`from`FlowDocument`to`Direct2D<br/>
edge Text-as-image`generation Heading`substitution<br/>
edge Graph`generation Subpixel`hinting`(future)<br/>
Получается такая вот картинка:
Это конечно DSLка, причем реализовал я ее через reflection. Каждая директива
edge
превращается в реальный вызов метода edge. Вот как выглядит парсер этих команд:public void AddInstruction(string line)<br/>
{<br/>
if (string.IsNullOrEmpty(line)) return;<br/>
foreach (Match m in Regex.Matches(line, "([\"'])(?:\\\\\\1|.)*?\\1"))<br/>
line = line.Replace(m.Value, m.Value.Replace(' ', '`'));<br/>
string[] parts = line.Split(' ').Select(p => p.Replace('`', ' ').Unquote()).ToArray();<br/>
if (parts.Length < 1) return;<br/>
// having acquired the parts, see if there's a matching method
MethodInfo mi = null;<br/>
if (cachedMethodInfo.ContainsKey(new KeyValuePair<string,int>(parts[0], parts.Length - 1)))<br/>
mi = cachedMethodInfo[new KeyValuePair<string, int>(parts[0], parts.Length - 1)];<br/>
else<br/>
{<br/>
var methods = GetType().GetMethods().Where(m => m.Name.ToLower() == parts[0]<br/>
&& m.GetParameters().Length == (parts.Length - 1));<br/>
if (methods.Any())<br/>
{<br/>
mi = methods.First();<br/>
cachedMethodInfo.Add(new KeyValuePair<string, int>(mi.Name, parts.Length - 1), mi);<br/>
}<br/>
}<br/>
if (mi != null)<br/>
{<br/>
var pars = mi.GetParameters();<br/>
object[] ps = new object[0];<br/>
if (pars.Length == parts.Length - 1)<br/>
{<br/>
// try building a parameter array
ps = new object[pars.Length];<br/>
for (int i = 0; i < pars.Length; i++)<br/>
{<br/>
var par = pars[i];<br/>
var source = parts[i + 1];<br/>
ps[i] = ConvertString(source, par.ParameterType);<br/>
}<br/>
}<br/>
// parameters ready - call it
try { mi.Invoke(this, ps); }<br/>
catch (Exception ex) { }<br/>
}<br/>
}<br/>
В кусочке кода выше, я постарался закэшировать дорогой поиск инфы о методе. Возможно есть решения и получше, но у меня все работает. Это в очередной раз показывает, что не всегда нужно использовать F# или лезть в DLR чтобы быстро получить интерпретатор маленькой DSL.
Вот собственно все, о чем хотел рассказать.
Заметки
- ↑ Вот у этой сноски цифра 1