Как стать автором
Обновить

MarkedText — маркдаун здорового человека

Время на прочтение10 мин
Количество просмотров40K

Здравствуйте, меня зовут Дмитрий Карловский и все свои статьипрезентации) пишу я в MarkDown разметке. И знаете что? Она уже порядочно меня подзаелозила! Тексты я пишу на русском, но большая часть спецсимволов есть только в английской раскладке клавиатуры. А редактирование таблиц - это вечная пизанская башня из вертикальных линий. Короче, есть у него проблемы как с удобством редактирования, так и с наглядностью представления. Так что давайте попробуем спроектировать его с нуля, не таща за собой килотонны головоломных конструкций.

Вёрстка несколько поехала, так как на Хабре выкатили новый кривой визивиг. Так что теперь писать статьи в маркдауне, а потом выкладывать на Хабр будет крайне сложно. С нормальной вёрсткой эту статью вы можете почить на гитхабе: https://github.com/nin-jin/HabHub/issues/39

Принципы

  • Однозначность синтаксиса

  • Простота синтаксиса

  • Единообразность синтаксиса

  • Минимальное влияние на естественный вид текста

  • Удобство редактирования независимо от раскладки

  • Наглядность представления

  • Расширяемость

  • Быстрая и надёжная запоминаемость

В качестве спец-символов форматирования лучше использовать такие, которые есть в любой раскладке, а не только в английской. То есть при прочих равных лучше отдать предпочтение следующим символам: ! " ; % : ? * ( ) _ + / \ . , - =

Существующие решения

В английской Википедии есть сравнительный обзор легковесных языков разметки, так что не будем повторяться. Там приведены следующие языки: AsciiDoc, BBCode, Creole, GitHub Flavored Markdown, Markdown, Markdown Extra, MediaWiki, MultiMarkdown, Org-mode, PmWiki, POD, reStructuredText, Textile, Texy, txt2tag.

Почти все они придерживаются примерно похожих конструкций. Однако, BBCode не является легковесным - это практически HTML с квадратными скобками вместо угловых. Поэтому его мы далее рассматривать не будем. Также проигнорируем и POD, чей синтаксис слишком многословный и не слишком наглядный.

Текстовые блоки

Блоки текста будем разделять двойным переводом строки, что даст визуальный отступ между блоками. Тип блока определяется специальным префиксом в начале каждой строки блока. Если префикс не распознан, то это обычный абзац.

Списки

Списки бывают двух типов: упорядоченные и неупорядоченные. Их можно вкладывать друг в друга в произвольной комбинации. В этом случае вложенные списки отображаются с отступом.

Элементы неупорядоченного списка предваряются маркером. По нормам многих языков таким маркером выступает тире. В вебе в основном потребляются "буллеты". В облегчённых языках разметки в основном используются следующие:

- item
* item
+ item

Из них лучше всего подходит на эту роль именно дефис - он выглядит похоже на тире и легко набирается с клавиатуры. Маркер и текст элемента списка отделяется пробелом. Таким образом суммарный отступ текста слева получается равным 2 - именно такой у нас будет отступ у вложенных списков, чтобы они были выровнены с текстом, к которому относятся. То есть останавливаемся на таком варианте:

- first
- second
  - first of second
    - first of first of second
  - second of second 
- third
  • first

  • second

    • first of second

      • first of first of second

    • second of second

  • third

Упорядоченные списки предполагают отображение увеличивающегося перед каждым элементом счётчика. Счётчик отделяется от текста элемента списка разделителем - точкой, скобочкой и тп. Некоторые языки предполагают ручное задание значений счётчика, что крайне не удобно поддерживать в актуальном состоянии:

1. item
2) item

Некоторые языки позволяют указать специальный маркер списка, который будет отображаться как счётчик - это уже куда удобнее:

# item

Правда символ решётки есть лишь в английской раскладке. Мы же воспользуемся для этого плюсиком, ведь к счётчику прибавляется по единичке на каждый элемент списка:

+ first
+ second
  + first of second
    + first of first of second
  + second of second 
+ third
  1. first

  2. second

    1. first of second

      1. first of first of second

    2. second of second

  3. third

Цитаты

Цитаты объединяют произвольные блоки текста, показывая, что их автором является кто-то другой. Распространённая практика - выделять цитаты угловыми скобками:

> quote
> - list in quote
> > inner quote

Однако, угловая скобочка есть лишь в английской раскладке. Тут гораздо лучше подходит символ кавычки, который уже используется в цитатах внутри строк и доступен не только в английской раскладке:

" quote
" - list in quote
" " inner quote

Таблицы

Таблицы - это двумерное представление данных. И многие языки стараются сохранять двумерность ради наглядности:

|=  |= table |= header |
| a | table  | row     |
| b | table  | row     |

|   | table | header |
|---|-------|--------|
| a | table | row    |
| b | table | row    |

First Header | Second Header
------------ | -------------
Content from cell 1 | Content from cell 2
Content in the first column | Content in the second column

Однако, такое представление сильно бьёт по удобству редактирования - приходится вручную постоянно выравнивать колонки.

И ладно бы только это, но такое представление обладает крайне слабой выразительностью - в ячейку можно поместить лишь один абзац. Туда нельзя поместить несколько абзацев, список, преформатированный блок и тд.

Наконец, даже с задачей наглядности такой синтаксис справляется плохо, когда в ячейках содержится больше пары слов - колонки уезжают далеко вправо и либо выпадают за скроллинг, либо начинают как попало переноситься, ломая всё выравнивание:

|   | table                                                                                                                          | header |
| a | There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable.  | row     |
| b | table                                                                                                                          | row     |

Короче, рисовать табличку в тексте - бессмысленное занятие. Однако можно сохранить двумерность представления и без таблицы - достаточно выравнивать содержимое ячеек, относящихся к разным колонкам, друг под другом, но с разным отступом:

! 
  ! table
    ! header
! a
  ! There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable.
    ! row
! b
  ! table 
    ! row

Таким образом человеку легко навигироваться по такой структуре как по горизонтали, так и по вертикали. А маркеры в виде восклицательных знаков похожи на вертикальные линии, но доступны не только в английской раскладке. Редактировать такое представление гораздо удобнее. Оно не боится внезапных переносов текста. А внутрь ячеек можно помещать любые другие блоки текста в произвольном количестве.

Заголовки

В некоторых языках заголовки выделяются разными типами подчёркиваний:

Level 1 Heading
===============

Level 2 Heading
---------------

Level 3 Heading
~~~~~~~~~~~~~~~

Выглядит, конечно, красиво, но набирать это утомительно, парсить сложно, а перепутать разные типы подчёркиваний - легко.

Есть более удобная форма, где уровень заголовка определяется числом спецсимволов в префиксе:

# Level 1 Heading #
## Level 2 Heading ##
### Level 3 Heading ###

Справа может быть такой же суффикс, несущий лишь декоративную роль, но набирать его утомительно. В разных языках используются разные спецсимволы:

## Level 2 Heading
== Level 2 Heading
** Level 2 Heading
!! Level 2 Heading
++ Level 2 Heading

Решётка есть лишь в английской раскладке. Плюсы, звёздочки, восклицательные знаки мы задействуем для другой семантики. А вот массивные символы равенства - то, что надо. Заголовки делят текст на секции, так что пара горизонтальных линий отлично это иллюстрируют:

= Level 1 Heading
== Level 2 Heading
=== Level 3 Heading

Преформатированный текст

В некоторых языках для преформатированного текста используются специальные кавычки:

```markdown
preformatted 
        text
```

Однако, если в таком тексте тоже нужно вывести эти кавычки, то получается облом. Поэтому предпочтительнее вариант с префиксом перед каждой строкой. Например, в качестве такого префикса может выступать отступ, равный 2 или 4 пробелам:

    preformatted 
            text

Преформатированный текст как правило не набирается напрямую при наборе текста, а копируется извне, где никаких префиксов нет кроме как раз отступов. Поэтому такой вариант довольно удобен, ведь во многих случаях такой текст можно просто вставить и больше ничего с ним не делать, чтобы он правильно отобразился.

В преформатированном тексте важен каждый символ, поэтому внутри него не может быть никакого форматирования. Однако, часто в повествовательных целях необходимо выделить какие-то строки или пометить их удалёнными/добавленными.

Поэтому мы разделим префикс на две части:

  • Маркер преформатированного текста из 2 пробелов.

  • Маркер форматирования строки из пары спец символов.

Выглядеть оно будет так:

    preformatted
            text
  --deleted
  --   text
  ++inserted
  ++    text
  **highlighted
  **       text

А как же указание языка для этого текста? А к чёрту его. Набирать каждый раз название языка утомительно. Откуда брать это название - не понятно. Да и что делать с языками, которые ещё не поддерживаются, - не ясно. Лучше уж тут работает автоматика - она должна либо сама распознать язык, либо использовать универсальную подсветку, подходящую большинству языков.

Инлайн форматирование

Типичный приём для добавления некоторым словам особого смысла - это окружить их специальными "кавычками". Такие кавычки должны достаточно редко встречаться в обычном тексте, чтобы не приходилось париться с экранированием, которое негативно сказывается на удобстве чтения. Как правило такие кавычки состоят из нескольких повторяющихся спец-символов. Рассмотрим варианты их числа:

  • 1 - слишком мало, высок риск, что символ обычного текста будет воспринят как форматирование.

  • 3 - слишком много, каждый раз трижды нажимать клавишу - слишком утомительно.

  • 2 - золотая середина, на этом и остановимся.

Акценты

Акценты служат для привлечения внимания, выделяя важные моменты, иносказания, коннотации и тп. Однако, в основном используются два вида: сильный и слабый акцент. И если назначение сильного акцента всем более-менее понятно, то слабый акцент используют для чего попало и приходится угадывать, что имел ввиду автор. Так что у меня нет уверенности стоит ли его поддерживать вообще, но пусть пока будет.

Варианты сильного акцента:

*strong*
**strong**
__strong__
'''strong'''
''strong''

Апострофы слишком похожи на кавычки и секунды, так что их сразу отбрасываем. Из оставшихся более распространён вариант с "массивными" звёздочкам - его и выберем.

**strong**

strong

Варианты слабого акцента:

'emphasis'
''emphasis''
_emphasis_
/emphasis/
//emphasis//
*emphasis*
~emphasis~

Тут уже разнообразие побольше. Однако, слабый акцент обычно выводится наклонным шрифтом, поэтому наиболее естественно смотрится наклонная линия, которая мгновенно укладывается в памяти.

//emphasis//

emphasis

Правки

Порой надо выделить часть текста, как удалённую, а часть как добавленную. Зачастую это выглядит как зачёркнутый и подчёркнутый текст. Оба варианта в той или иной мере осложняют чтение, поэтому предпочтительнее использовать для этого подсветку фона, как это делают инструменты для мержа. Но в этом случае символы подчёркивания и дефисы уже перестают ассоциироваться с соответствующим им типом форматирования.

Итак, типичные формы выделения добавлений:

_insertion_
__insertion__
+insertion+

И удалений:

~deletion~
~~deletion~~
-deletion-
--deletion--

Добавления и удаления естественным образом независимо от визуализации ассоциируются с плюсом и минусом, так что воспользуемся именно ими:

++insertion++
--deletion--
  • insertion

  • deletion

Ссылки

Ссылки бывают двух видов:

  • Гиперссылка - она переносит вас к цели при клике на неё. Для неё задаётся отображаемое содержимое и урл для перехода.

  • Встраивание - цель встраивается в просматриваемый документ. Для него задаётся урл, откуда берётся встраиваемый документ, и альтернативное содержимое, если встраивание по какой-либо причине не произошло (не поддерживаемый формат, не разрешённый домен, не удалось подключиться и тд).

Гиперссылки выглядят в разных языках так:

"Text":http://example.com
http://example.com[Text]
<http: example.com|text="">
[Text|http://example.com]
[[Text|http://example.com]]
[[http://example.com|Text]]
[Text http://example.com]
[http://example.com Text]
[Text](http://example.com)
`Text <http: example.com="">`_

А встраивания так:

![title](http://example.com/image.png)
{{http://example.com/image.png|title}}
.. image:: /path/to/image.jpg

Важно отметить, что обычно встраивать разрешено лишь изображения. Если хочется встроить видео или сайт - в лучшем случае предлагается писать HTML. Мы же позволим встраивать что угодно - проверка безопасности для урлов должна происходить системно и регулироваться отдельно. Язык разметки текста никак не должен их регламентировать.

Для однозначной интерпретации разметки нам потребуется открывающая и закрывающая кавычки, а так же разделитель между урлом и содержимым. В качестве спец-символов должны быть такие, которые не могут встретиться в урле, и которые легко вводить в любой раскладке. Таких символов всего два: \ и ".

Отметим, что встраивание - это фактически цитирование стороннего ресурса, так что кавычка для этого более чем уместна. В качестве же разделителя ссылки на ресурс и его альтернативного представления, лучше подойдёт \, который крайне редко встречается в обычном тексте, в отличие от кавычки.

""Embedded image\http://example.org/favicon.ico""
""Embedded video\https://youtube.com/video=1234""
""Embedded site\https://marked.hyoo.ru/""

Если альтернативное содержимое опущено, то в качестве него должен быть взят сам урл. То есть следующие два варианта дают идентичный результат:

""http://example.org/favicon.ico""
""http://example.org/favicon.ico\http://example.org/favicon.ico""

Для гиперссылок же воспользуемся \ во всех местах:

\\Clickable text\http://example.org/\\
Clickable url: \\http://example.org/\\

Разумеется разные типы ссылок можно комбинировать. Например, сделать картинку-ссылку можно так:

\\""Example\http://example.org/favicon.ico""\http://example.org/\\

Инлайн код

Инлайн код выводится моноширинным шрифтом и выключает внутри себя любое форматирование. В каждом языке используется что-то своё:

+monospace text+
`monospace text`
``monospace text``
```monospace text```
|monospace text|
{{monospace text}}
{{{monospace text}}}
=code=
~verbatim~
@monospace text@
@@monospace text@@

Из доступных не только в английской раскладке тут есть только + и =, которые у нас уже задействованы. То есть нужно придумывать что-то своё. В разных языках используются разные символы, так что идеального варианта быть не может. Но кажется ;; к нему максимально близко. Двойная точка с запятой как правило не имеет смысла во многих языках.

;;monospace text;;

monospace text

Резюме

Теперь давайте соберём все конструкции формата в одной короткой шпаргалке:

= MarkedText

Формат текста с **легковесным форматированием**.

--

== Принципы

+ Синтаксис:
  - Однозначность
  - Простота
  - Единообразность
+ Внешний вид:
  - Минимальное влияние на естественный вид текста
  - Наглядность форматирования
+ Редактирование:
  - Независимость от раскладки
  - Быстрая и надёжная запоминаемость

== Cравнение с альтернативами

! **Язык**
  ! **Плюсы**
    ! **Минусы**
! MarkedText
  ! - Удобное редактирование таблиц.
  ! - Поддержка сложного форматирования внутри ячеек.
  ! - Простота реализации.
  ! - Легко запоминающийся консистентный синтаксис.
  ! - Удобство редактирования в русской раскладке.
  ! - Колонки не расползаются далеко вправо за горизонтальный скроллинг и не переносятся на новую строку.
    ! - Не поддерживается пока что никакими сторонними инструментами.
! MarkDown
  ! - Широкая поддержка различными инструментами.
  ! - Наглядное представление таблиц.
    ! - Сложности с редактированием таблиц.
    ! - Сильно ограниченное содержимое ячеек.

== Парсинг

    const res = [ ... $hyoo_marked_line.parse( '**text**' ) ]
  --$mol_assert_equal( res[0].strong, '**text**' )
  ++$mol_assert_equal( res[0].marker, '**' )
  **$mol_assert_equal( res[0].content, 'text' )

== Отзывы

" " " Типичный пользователь: Нигде не поддерживается, идите в --жопу-- ++Жодино++ с таким синтаксисом!
" " 
" " Но мы же программисты, мы можем это исправить.. Для этого даже не надо быть экспертом ни по ;;C++;; , ни по ;;D++;;..
" 
" Никому это не нужно (с) Диванный Эксперт

Тем не менее, это полезное упражнение в проектировании.

== Ссылки

- Песочница: \\https://marked.hyoo.ru/\\
- \\Статья о MarkedText\https://github.com/nin-jin/HabHub/issues/39\\
- \\Парсер на TS\https://github.com/hyoo-ru/marked.hyoo.ru/\\
- \\Конвертер в HTML на TS\https://github.com/hyoo-ru/marked.hyoo.ru/tree/master/to/html\\
- ""Результат билда $mol_regexp\https://github.com/hyoo-ru/mam_mol/workflows/mol_regexp/badge.svg""

Ссылки

Обратная связь

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 69: ↑50 и ↓19+53
Комментарии155

Публикации

Истории

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань