MagicString — это малоизвестная библиотека. Не смотря на это она решает одну из насущных проблем — изменение исходного кода с использованием его структуры (AST — abstract syntax tree).

В этой статье мы узнаем, что такое MagicString и такие ли уж эти строки «магические». Это поможет нам понять следующую статью в в которой я расскажу, как удалось перевести документацию Angular так быстро, и как это поможет с созданием универсального переводчика как Markdown, так и файлов любого другого формата.





2 недели назад я зарелизил русскоязычную документацию Angular (angular24.ru). За это время было добавлено 35 issues с правками по тексту и 2 pull request-а. Я искренне сомневался, что система, в которой ты выделяешь текст, предлагаешь перевод и автоматически презаполняется issue на GitHub, будет работать. Но crowdsourcing работает! :) Подробнее об этом можно узнать из этой статьи.

После релиза один из самых задаваемых вопросов был: «А зачем?». Вопрос совершенно правильный, но чтобы на него ответить, нужно сначало понять, что такое MagicString, как он работает и чем он полезен.

Предоложим, что у нас есть простой исходный код:

const a = 1;

Мы хотим заменить const на var. Самое простое решение — заменить const на var с помощью обычного String.prototype.replace. И для данной задачи это, скорее всего, самое правильное решение. Но что если нам нужно заменить const на var только в глобальной области видимости? При этом не заменять их внутри функций? Можно, конечно, придумать более сложную регулярку или написать хитрый код, но есть более маштабируемый и гибкий способ.

Мы можем с помощью парсеров получить AST — Abstract Syntax Tree. Если интересно, что из себя представляет AST, то зайдите на astexplorer.net. По сути это дерево которое в точности отображает структуру вашего кода.

Дальше у каждой из Nodes в этом AST есть start и end индексы, указывающие на позиции данных элементов в исходном коде. Зная эти координаты и имея под рукой структуру документа, мы может делать сложные замены и перестановки с сохранением структуры документа.



Обычно замена происходит с помощью дизайн паттерна visitor и нескольких helpers, которые обычно оборачиваются в одну библиотеку, которую можно назвать «transformer API». Для каждого парсера есть свой «transformer API».

Такие библиотеки очень удобны в работе, но у них есть несколько проблем. Одна из них — производительность.

Так как каждая (ну почти каждая) Node в AST дереве содержит координаты, то при изменении 1 ноды нам нужно зачастую обновить координаты для всего остального дерева. Тут вы можете поспорить, что можно обойтись малой кровью — не обновлять координаты везде, а просто отрендерить AST обратно в текст на основе структуры. Но тут есть 1 проблема: вы сразу же потеряете форматирование оригинального текста, что противоречит нашей задаче — заменить const на var в существующей строке. По факту мы получим новую строку с новым форматированием. И если для маленькой строки это не проблема, то представьте файл из 1000 строк, в котором полностью изменилось форматированние из-за замены const на var. Звучит не очень.



И тут на помощь приходит магия MagicString. Впервые я узнал об их существовании из проекта Rich Harris, который назывался butternut. Butternut — это минификатор JavaScript. Заявлялось, что butternutt быстрее UglifyJS в 3 раза и Babili — в 10-15 раз. Забегу вперед и скажу, что проект накрылся медным тазом как минимум 3 года назад. Но еще тогда меня заинтриговал секрет его производительности. Это был MagicString.

Давайте взглянем на работу с MagicString:

var MagicString = require( 'magic-string' );
var s = new MagicString( 'const a = 1; const b = 2;' );

s.overwrite( 0, 5, 'var' );
s.toString(); // 'var a = 1; const b = 2;'

// другие операции

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

  1. Мы заменили const на var, начиная с индекса 0 и заканчивая индексом 5
  2. Мы знаем, что все последующие замены должны иметь index на 2 меньше (var меньше const на 2 символа, строка короче)
  3. Мы обновляем координаты всех операций
  4. Применяем следующую операцию и т.д.




Все выглядит довольно просто. Но почему MagicString быстрее? Ответ довольно простой: количество операций, которые мы производим над нашим деревом, значительно меньше, чем количество AST нод. Не говоря уже о количестве памяти, необходимой для AST и о том, что Tree Traversal (путешествие по дереву) не бесплатная операция, а O(n+m)



А если я готов подождать дополнительные полчаса? И тут вступает второй плюс MagicString. Каждый парсер изобретает свое API для трансформации. И это еще очень хорошо, если такое API есть (далеко не каждый парсер его предоставляет), очень часто мы остаемся без возможности нормально заменять исходный текст с использованием AST. А вот MagicString — это единое универсальное API для изменения исходной строки. Не важно, каким парсером или комбинацией парсеров вы пользовались. С помощью MagicString вы можете одинаково работать с любым AST.



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

Подписывайтесь на мой Telegram канал @obenjiro_notes и Twitter obenjiro, чтобы не пропустить следующие статьи по теме и много всего другого интересного.