Парсер для CommonMark

Все началось с того, что мне понадобился парсер для Markdown, который строит AST, а не пытается сразу выдавать готовый HTML. А ещё было желательно, чтобы парсер был написан на Dart, так как проект я собирался писать именно на этом языке. Но, к сожалению, обнаружился только один парсер для Markdown, написанный на Dart. Поэтому идею пришлось отложить до лучших времён и сесть за написание своего велосипеда парсера.

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

И вот, спустя месяц, я рад вам представить свою реализацию, которая проходит все тесты CommonMark. И да, она позволяет получить AST для последующей обработки.

Библиотека md_proc на GitHub, pub.dartlang.

В самых ближайших планах реализовать восстановление Markdown обратно из AST, а также поддержка самых распространённых и нужных расширений, вроде TexMath, Footnotes, Smart punctuation и других. Ну и, конечно, поддержка совместимости с CommonMark тоже обязательно будет, тем более, что CommonMark ещё должен немного поменяться до того, как будет опубликована официальная версия 1.0. И уже после этого можно будет вернуться к первоначальному проекту.

Dart и highlight.js

Когда я создавал этот блог, нужно было что-то делать с подсветкой синтаксиса для примеров кода, которые здесь появляются. После рассмотрения различных альтернатив (уже и не помню, каких именно) я остановился на highlight.js. На тот момент эта библиотека поддерживала все необходимые мне языки. А потом я решил поэкспериментировать с
Dart… Ну и написать пару постов о нём. Смотрю, а поддержки Dart в highlight.js нет. На сайте проекта сказано, что разработчики не выполняют заказы на добавление новых языков, если хотите поддержку нового языка, то реализуйте её сами.

Что делать, реализовал поддержку и отправил pull request на добавление языка в библиотеку. А пока использовал свою сборку из своего репозитория. Прошло полгода — и теперь мой патч наконец включили в библиотеку! Более того, новая версия 8.2 уже вышла и вы можете ей воспользоваться, чтобы добавить поддержку подсветки исходного кода на Dart (и других языках) к себе на сайт.

Проблемы с запуском планировщика

Поделюсь некоторыми сложностями, с которыми я столкнулся, запуская планировщик маршрута.

Конечно, во время самой разработки все сложности сводились к алгоритмическим. В Dartium всё работало как положено с нужным количеством ошибок и предупреждений. Проблемы начались сразу после компиляции в JavaScript и запуске в обычном браузере (кто бы сомневался). Проект не запустился, выдав странного вида stack trace. Ну что же, пересобираем проект без минификации. Stack trace стал читаемым, ошибки понятными. Как оказалось, вся проблема была в аннотации @MirrorsUsed. Дело в том, что mirrors (так называется reflection в Dart) занимает непозволительно много места в скомпилированном коде. И для того, чтобы этого избежать, авторы Dart предлагают использовать эту аннотацию для указания библиотек и классов, информацию о которых нужно будет получить во время исполнения (кстати, в будущем обещают доработать компилятор, что избавит от необходимости использовать эту аннотацию). Так вот, мой класс контроллера не был указан в @MirrorsUsed и, соответственно, AngularDart не мог с ним работать.

Исправил, скомпилировал, не запустилось. Правда, в этот раз я увидел несколько больше, чем просто пустую страницу. Немного поисков — и обнаружилась вторая проблема: нельзя в шаблонах AngularDart использовать методы встроенных объектов Dart. То есть в Dart можно, а в JavaScript нельзя. Конкретно в моем случае было написано:

{{segment[0].distanceTo(segment[1]).round()}}

Понятное дело, JavaScript и не догадывается о методе round у типа double. Да, собственно, про существование double он тоже не догадывается. Поэтому нужно писать геттер или вспомогательную функцию. Например так:

{{segment[0].distanceToRound(segment[1])}}

Ура, запустилось! Теперь осталось собрать минифицированную версию и выложить. Но после сборки проект снова упал с непонятным stack trace, как и в самом начале.

Не один час был потрачен на гугление всевозможных причин. Дело почти дошло до того, чтобы написать новый багрепорт о неправильной компиляции. Но я вовремя остановился, и хорошо, потому что ошибка была у меня и, кстати, довольна простая: я пытался проинициализировать Яндекс.Карты, а функция-конструктор не существовала. Вот проблемная строка, она соответствует map = new ymaps.Map(“map”, options); в JavaScript:

map = new JsObject(context['ymaps']['Map'], ["map",  options]);

Но ведь несжатая версия работает! Моё предположение, что сжатая версия приложения инициализируется значительно быстрее и поэтому API Яндекс.Карт ещё не готово к тому времени, оказалось верным, и вызов функции ymaps.ready всё исправил.

Итог. Более-менее сложное Dart-приложение хорошо работает в браузерах без поддержки Dart. Но зато когда сталкиваешься с проблемой, которая возникает только в скомпилированном и минифицированном коде, можно потратить довольно много времени на поиск решения просто потому, что подсказок никто не даст, а код выглядит довольно запутанно. Но в своей практике я сталкивался со случаями, когда и обычное JavaScript-приложение после сжатия начинало вести себя неправильно и возникали те же проблемы поиска решения.

Хочется отметить минификатор JS, встроенный в Dart. Он заменяет все ; на переводы строк. Из-за этого в результате получается файл со множеством строк, а не с одной, как в случае с другими минификаторами. А это, в свою очередь, сильно упрощает ручной анализ кода.

И снова Dart

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

  1. Поддержка Dart в продуктах JetBrains просто отличная. Писать на Dart гораздо удобнее, чем на JavaScript, а ведь плагин для JavaScript разрабатывается и улучшается намного дольше. И всё это из-за более строгой структуры языка. Она позволяет IDE лучше просчитывать зависимости и в нужное время дополнять необходимые конструкции. Но не обошлось без ложки дёгтя. Иногда плагин просто перестаёт дополнять что бы то ни было, спасает только перезагрузка среды. И ещё не хватает привычных intentions, вроде склеить два if в один или заменить одинарные кавычки на двойные, но я думаю, что это будет реализовано со временем.

  2. В Dart есть приватные члены класса, но нет зарезервированного слова private. Всё потому, что все идентификаторы, начинающиеся с подчёркивания (_), считаются приватными. И это правильно.

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

    class Singleton {
      static final Singleton _singleton = new Singleton._internal();
      factory Singleton() {
        return _singleton;
      }
      Singleton._internal();
    }
    
    var singleton = new Singleton();

    В этом же примере можно видеть ещё одну особенность Dart: именованные конструкторы. Так как в динамическом языке нельзя сделать перегрузку методов по типам параметров, то для конструкторов авторы решили добавить возможность выбора конструктора по имени. А для остальных случаев вполне хватает именованных необязательных параметров.

  4. В языке довольно много различного синтаксического сахара: тот же cascade operator (..). Крайне удобная штука для работы с DOM и стилями.

    element.style
      ..visibility = 'visible'
      ..display = 'block';
  5. Ещё прекрасно сделана подстановка значений переменных и выражений в строку. Это выглядит частью языка, а не afterthought, как, например, в PHP.

    previousLink.attributes['title'] += ' (${isMac ? '⌥←' : 'Ctrl + ←'})';

    Обращаем внимание на то, что внутри ${...} может быть любое выражение точно в таком же виде, в каком вы бы его увидели и без строки. Кавычки внутри выражения не закрывают строку, и их не нужно экранировать, что вполне логично, но в том же PHP этого нет.

  6. Dart — язык с необязательной типизацией. То есть если хочешь, можно указать тип переменной, а можно просто написать var и не париться. Всё это будет работать до тех пор, пока не попадётся, например, функция с указанным типом параметров. Вот тут-то тип твоей переменной будет проверен. И будет выведена ошибка, если типы не совпадают. Но когда вместо var указываешь реальный тип переменной, то проверки происходят гораздо чаще, буквально в каждом операторе, везде, где может возникнуть несовместимость типов.

    Кроме того, в Dart нет неявного приведения типов. Например, вот этот код выведет сообщение об ошибке потому, что нельзя просто так привести строку к булевому значению:

    String str = '';
    if (!str) {
      // do something
    }

    Но не стоит особо волноваться: о большинстве потенциальных проблем статический анализатор языка (то есть IDE) сообщит заранее.

    Все эти проверки работают в так называемом «checked mode». Чтобы этот режим включить, виртуальная машина должна быть запущена со специальным параметром, например:

    dart --checked main.dart

    Или в случае с сайтом сам браузер нужно запускать так (пример для Linux):

    DART_FLAGS='--checked' ./chrome

    Если этого не сделать, то виртуальная машина будет работать в production-режиме и ни одной дополнительной проверки не будет сделано. Зато выполняться скрипт будет очень быстро! Поэтому можно думать о типах, как об автоматических инструкциях assert в языке в помощь разработчику. Кстати, просто assert тоже присутствует и тоже не работает в production-режиме.

    А ещё в Dart есть Generics. И это позволяет создавать весьма сложные зависимости типов и опять же проверки. К сожалению, Generics можно применить только к классам, может, в будущем добавят поддержку отдельных функций.

    class ParseContext<S extends Iterable> {
      final S source;
    
      // More definitions
    }
  7. Переопределение операторов. Не думаю, что нужно рассказывать про плюсы и минусы этого решения. Все, кто когда-либо сталкивался с ними в любом другом языке, и так о них знают. Динамический Dart может добавить к этому ещё немного проблем. И хотелось бы иметь возможность создания произвольных операторов, как в Haskell. Было бы круто.

  8. Из недостатков можно выделить то, что интерфейсы визуально не отличаются от классов. То есть нет зарезивированного слова interface, вместо него используется тот же class. Но при этом есть слова и extends, и implements. Всё различие заключается в том, что может присутствовать в классе, но не может присутствовать в интерфейсе, и наоборот. К примеру, в интерфейсе может быть factory-конструктор, который будет возвращать реализацию этого интерфейса по-умолчанию. Но в этом случае не получится унаследоваться от такого интерфейса при помощи extends. Компилятор выдаст «The generative constructor expected, but factory found». А это не особо очевидно, особенно когда видишь это сообщение первый раз.

  9. Ещё один минус — размер js-файла на выходе. Пока не у всех есть браузер с нативной поддержкой Dart, и многим придётся загружать сконвертированный код. Ладно, будем честными: браузера пока ни у кого нет, кроме разработчиков, которые на Dart пишут (он называется Dartium, его можно скачать на сайте проекта). Так вот, сгенерированный JavaScript для этого блога весит 207 Кб (сравните с 8 Кб минифицированного Dart-кода). А всё потому, что в js-файл компилятору приходится встраивать всю ту огромную прослойку для поддержки разницы в DOM и событиях между языками.

Я думаю, что Google не оставит своих планов выпустить nextgen язык для Web. Меня лично эти планы вполне устраивают, так как я отлично представляю, чем этот язык будет лучше и удобнее. Как минимум он позволит выбросить весь тот backward-compatibility хлам, который уже давно никто не использует, но который обязан присутствовать в каждом браузере, потому что его могут использовать сайты, созданные 15 лет назад.

И не надо говорить, что всякие надстройки используют те, кто не осилил выучить нативный JavaScript.

P. S. К сожалению, нормальной поддержки подсветки Dart в блоге пока нет, но я надеюсь это в скором времени исправить. Исправил.

Dart

Очередной эксперимент. В этот раз я решил переписать весь JavaScript-код этого блога на новомодном Dart. И если результат я не выложу в итоге на рабочий сервер, это всё равно неплохая возможность взглянуть на новый язык. Почему именно Dart? Даже и не знаю. Мне подход с написанием языка с нуля нравится больше, чем попытки исправить недостатки JavaScript с помощью всяких надстроек, как в случае TypeScript или CoffeeScript. Я не говорю, что это плохие языки, просто они пытаются обойтись «малой кровью», а Google, создатель Dart, играет по крупному.

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

var sum = (a, b) {
     return a + b;
};

Или даже ещё короче:

var sum = (a, b) => a + b;

Но это мелочи. Гораздо интереснее исправленные области видимости. Например, переменная, объявленная внутри цикла for, не видна вне его. Очень понравилось, как в Dart переделали всю структуру DOM-объектов. Например, у элементов свойство textContent превратилось в text, а childNodes в nodes. Наконец-то attributes — это явный ассоциативный массив, а не NamedNodeMap плюс набор методов вроде getAttribute и setAttribute. И classes — это множество, а не строка. Красота!

События тоже претерпели сильные изменения. Нет больше element.onclick и element.addEventListener (attachEvent в старых IE). Теперь события представляют собой потоки, на которые можно подписываться. Теперь нужно писать:

button.onClick.listen((event) {
    // do something
});

А если нужно, чтобы обработчик сработал только один раз:

button.onClick.first.then((event) {
    // do something
});

Свойство first в примере выше является Future, что по сути то же, что и Promise. Я, кстати, не знаю, в чём разница между Futures и Promises, буду рад, если кто-нибудь объяснит.

Конечно, вся эта красота не приходит просто так. И если текущая сжатая версия скриптов для блога на языке Dart занимает 13 кб (по большей части из-за подключённой библиотеки интернационализации), то она же, но скомпилированная в JavaScript, все 134.

Текущая версия скриптов тут. Комментарии не возбраняются, а очень даже приветствуются.

Моложе →