Конвертировать между элементами Markdown

Какие есть варианты для анализа документа Markdown и обработки его элементов для вывода другого документа Markdown?

Скажем так

```
# unaffected #
```

# H1 #

H1
==

## H2 ##

H2
--

### H3 ###

следует преобразовать в

```
# unaffected #
```

## H1 ##

H1
--

### H2 ###

### H2 ###

#### H3 ####

в среде Node. Целевой элемент может варьироваться (например, #### может быть преобразован в **).

Документ может содержать другие элементы разметки, которые должны остаться неизменными.

Как это можно получить? Очевидно, что не с регулярными выражениями (использование регулярного выражения вместо полноценного лексера повлияет на # unaffected #). Я надеялся использовать marked но похоже, что он способен только на вывод HTML, а не на Markdown.

5 ответов

Решение

Рассматривали ли вы использование HTML в качестве промежуточного формата? Находясь в HTML, различия между типами заголовков будут неразличимы, поэтому преобразование Markdown -> HTML эффективно их нормализует. Существует множество уценок -> конвертеров HTML, а также ряд HTML -> уценок.

Я собрал пример, используя эти два пакета:

Я не знаю, есть ли у вас какие-либо требования к производительности (читай: это медленно...), но это очень низкое инвестиционное решение. Взглянуть:

var md = require('markdown-it')(),
    h2m = require('h2m');

var mdContent = `
\`\`\`
# unaffected #
\`\`\`

# H1 #

H1
==

## H2 ##

H2
--

### H3 ###
`;

var htmlContent = md.render(mdContent);
var newMdContent = h2m(htmlContent, {converter: 'MarkdownExtra'});
console.log(newMdContent);

Возможно, вам придется поиграть со смесью компонентов, чтобы получить правильную поддержку диалекта и еще много чего. Я попробовал кучу и не смог сравниться с вашим выводом. Я думаю, что, возможно, -- интерпретируется по-разному? Вот вывод, я позволю вам решить, достаточно ли он хорош:

```
# unaffected #

```

# H1 #

# H1 #

## H2 ##

## H2 ##

### H3 ###

Вот решение с внешним анализатором уценки, pandoc, Это позволяет настраивать фильтры в haskell или python для изменения ввода (также есть порт node.js). Вот фильтр питона, который увеличивает каждый заголовок на один уровень. Давайте сохраним это как header_increase.py,

from pandocfilters import toJSONFilter, Header

def header_increase(key, value, format, meta):
    if key == 'Header' and value[0] < 7:
        value[0] = value[0] + 1
        return Header(value[0], value[1], value[2])

if __name__ == "__main__":
    toJSONFilter(header_increase)

Это не повлияет на блок кода. Однако он может преобразовать заголовки в стиле setex для элементов h1 и h2 (используя === или же ---) в заголовки в стиле atx (используя #), и наоборот.

Чтобы использовать скрипт, можно вызвать pandoc из командной строки:

pandoc input.md --filter header_increase.py -o output.md -t markdown

С node.js вы можете использовать pdc для вызова pandoc.

var pdc = require('pdc');
pdc(input_md, 'markdown', 'markdown', [ '--filter', './header_increase.py' ], function(err, result) {
  if (err)
    throw err;

  console.log(result);
});

Несмотря на кажущуюся простоту, Markdown на самом деле несколько сложен для анализа. Каждая часть основывается на следующей, так что для охвата всех крайних случаев необходим полный анализатор, даже если вы хотите обработать только часть документа.

Например, различные типы элементов уровня блока могут быть вложены в другие элементы уровня блока (списки, цитаты и т. Д.). Большинство реализаций полагаются на различный определенный порядок событий в синтаксическом анализаторе, чтобы гарантировать, что весь документ проанализирован правильно. Если вы удалите одну из более ранних частей, многие из более поздних частей сломаются. Например, разметка Markdown внутри блоков кода не анализируется как Markdown, потому что одним из первых шагов является поиск и идентификация блоков кода, чтобы последующие этапы синтаксического анализа никогда не видели блоки кода.

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

Есть в основном три стиля анализаторов Markdown (я обобщаю здесь):

  1. Используйте подстановку регулярных выражений, чтобы поменять разметку Markdown для HTML Markup в исходном документе.
  2. Используйте рендер, который вызывается анализатором (на каждом шаге), когда он анализирует документ, выводящий новый документ.
  3. Создайте объект дерева или список токенов (особенности зависят от реализации), который будет представлен (преобразован в строку) в новый документ на более позднем этапе.

Исходная ссылочная реализация (markdown.pl) относится к первому типу и, вероятно, бесполезна для вас. Я просто упоминаю это для полноты.

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

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

function multiplyString (str, num) {
    return num ? Array(num + 1).join(str) : "";
}

renderer.heading = function (text, level) {
    return multiplyString("#", level+1) + " " + text;
}

Конечно, вам также необходимо создать средства визуализации для всех других методов визуализации уровня блока и встроенных методов визуализации, которые выводят синтаксис Markdown. Смотрите мои комментарии ниже относительно рендеров в целом.

Markdown-JS относится к третьему варианту (как оказалось, Marked также предоставляет API более низкого уровня с доступом к токенам, поэтому его можно использовать и таким образом). Как указано в README:

Промежуточное Представительство

Внутренне процесс преобразования фрагмента Markdown в фрагмент HTML состоит из трех этапов:

  1. Разобрать Markdown в дерево JsonML. Все ссылки, найденные при разборе, сохраняются в хеше атрибута корневого узла под ключом. references,
  2. Преобразуйте дерево уценки в дерево HTML. Переименуйте любые узлы, которые в этом нуждаются (bulletlist в ul например) и поиск любых ссылок, используемых ссылками или изображениями. Удалите атрибут ссылки после того, как сделано.
  3. Стригируйте HTML-дерево, стараясь не разрушать пробелы, где пробелы важны (например, окружающие встроенные элементы).

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

Вы можете взять объект дерева на шаге 1 или шаге 2 и внести свои изменения. Однако я бы порекомендовал шаг 1, так как дерево JsonML будет более точно соответствовать фактическому документу Markdown, поскольку дерево HTML на шаге 2 является представлением HTML, который должен быть выведен. Обратите внимание, что HTML потеряет некоторую информацию относительно оригинальной Markdown в любой реализации. Например, были ли звездочки или подчеркивания использованы для выделения (*foo* против _foo_) или в качестве маркера списка использовались звездочка, тире (дефис) или знак плюс? Я не уверен, сколько деталей содержит дерево JsonML (я не использовал его лично), но оно должно быть больше, чем дерево HTML на шаге 2.

После того, как вы внесли изменения в дерево JsonML (perhpas, используя один из перечисленных здесь инструментов, вы, вероятно, захотите пропустить шаг 2 и реализовать свой собственный шаг 3, который визуализирует (структурирует) дерево JsonML обратно в документ Markdown.

И в этом заключается трудная часть. Парсеры Markdown очень редко выдают Markdown. Фактически, парсеры Markdown очень редко выводят что-либо, кроме HTML. Самым популярным исключением является Pandoc, который является конвертером документов для многих форматов ввода и вывода. Но, желая остаться с решением JavaScript, любая библиотека, которую вы выберете, потребует от вас написания собственного средства визуализации, которое будет выводить Markdown (если только при поиске не появится средство визуализации, созданное какой-либо другой третьей стороной). Конечно, как только вы сделаете это, если вы сделаете это доступным, другие могут извлечь из этого пользу в будущем. К сожалению, построение рендерера Markdown выходит за рамки этого ответа.

Один из возможных путей при построении рендерера состоит в том, что если используемая вами библиотека Markdown сохраняет информацию о местоположении в своем списке токенов (или каким-либо другим способом предоставляет вам доступ к исходному необработанному Markdown для каждого элемента), вы можете использовать эта информация в рендерере просто скопировать и вывести оригинальный текст Markdown, кроме случаев, когда вам нужно изменить его. Например, библиотека markdown-it предлагает эти данные на Token.map и / или Token.markup свойства. Вам все еще нужно создать свой собственный рендерер, но проще сделать так, чтобы уценка была больше похожа на оригинал.

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

Вы должны использовать регулярные выражения. marked Сам используй Regexp для разбора документа. Почему не ты?

Вот некоторые из регулярных выражений, которые вам нужны, из исходного кода mark.js на github:

var block = {
  newline: /^\n+/,
  code: /^( {4}[^\n]+\n*)+/,
  fences: noop,
  hr: /^( *[-*_]){3,} *(?:\n+|$)/,
  heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,
  nptable: noop,
  lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,
  blockquote: /^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,
  list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,
  html: /^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,
  def: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,
  table: noop,
  paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,
  text: /^[^\n]+/
};

Если вы действительно не хотите использовать регулярные выражения, вы можете разветвить marked объект. и оверид Renderer объект.

Помеченный на github разделен на две составляющие. Один для разбора и один для визуализации. Вы можете легко изменить рендер на свой собственный рендер. (Компилятор)

Пример одной функции в Render.js:

Renderer.prototype.blockquote = function(quote) {
  return '<blockquote>\n' + quote + '</blockquote>\n';
};)

Может быть, это неполный ответ. Скопируйте без изменений в другой файл.

Затем заменить все

  1. #space с ##space

  2. space# с space##

Другие вопросы по тегам