Ещё один препроцессор для JavaScript

Интересна ли вам возможность писать в JavaScript вот такие штуки?

$ ('.btn-toogle').onclick (lambda $ (this).toggleClass ('active'));

var chars = Array.prototype.forEach.call ('0123456789abcdef', lambda arg.charCodeAt (0));

function fn (arg1 = 100, arg2 = 'test'){ ... }

var html = h`
    <div>
        <a href="...">link</a>
    </div>`;

for (var key, value in object) 
    console.log (key + ' = ' + value);


Тогда, возможно, вас заинтересует эта статья.

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

Препроцессор написан на Node.JS, причём для парсинга JavaScript'а используется генератор парсеров PEG.js. Кстати, одной из сильных сторон препроцессора (ну, лично для меня) является то, что обычный JavaScript-код отлично парсится. Ну, если, конечно же, в коде не встречаются ключевые слова вроде lambda, module, import или export.

Конечно, парсер на JavaScript не отличается производительностью, но, к счастью, кеширование вполне это компенсирует.

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

1. Сокращённая запись функций.


var way0   = lambda arg.charCodeAt (0),
    way1   = lambda (arg) arg.charCodeAt (0),
    way2   = lambda { return arg.charCodeAt (0) },
    way3   = lambda (arg){ arg.charCodeAt (0) },
    result = function (arg){ return arg.charCodeAt (0) };

Все четыре способа записи дают одинаковый результат. Аргумент «arg» добавляется тем функциям, у которых отсутствуют обычные аргументы. Если всё тело функции состоит из одной операции (statement; если честно, не знаю, как корректно сказать по-русски), функция вернёт результат этой операции.

Вот ещё пара примеров:
var // Вернёт сумму четырёх аргументов
	example0 = lambda (a, b, c, d) a + b + c + d,   

	// Некий аналог forEach
    example1 = lambda (a, fn) 
    	for (var i = 0; i < a.length; i ++)
    		fn (a [i]),	

    // Вернёт объект с полями a и b.							
    example2 = lambda { 
    	a: arg * 2, 
    	b: arg
    };


2. Аргументы функций по умолчанию.


Ничего особенно, заголовок уже говорит сам за себя.
function fn (arg1 = 100, arg2 = 'test'){ ... }

Аргументы будут сравниваться (без приведения типов) с undefined. Если вдруг они окажутся равны, им будут выставлены значения по умолчанию.

3. Многострочные строки (прошу прощения за столь некрасивое выражение).


Для начала, это просто способ записи строк с использованием символа «`» как кавычек. Можно наконец-то перестать расставлять «\» в концах строк (хотя, признаюсь, никто обычно не пишет так, чтобы были нужны многострочные строки). Но это ещё не всё. В записимости от символа перед открывающей кавычкой препроцессор может как-то по-особенному обработать текст внутри. Напишите «h» и препроцессор сожмёт строку как html-код, «c» — как css-код, «s» — удалит идущие подряд пробелы, «l» — сконвертирует LESS внутри строки в CSS (ну и сожмёт тоже), а «lp» — мой любимый :) — не только сконвертирует LESS в CSS, но и оставит возможность в дальнейшем определять свойства через JavaScript. Довольно удобно.

4. Модули.


Если коротко, это в каком-то смысле аналог классов из той же Java. Но ни в коем случае не обычных классов. В JavaScript же уже есть прототипы, верно?

Модули заменяют собой то, что в других языках обычно называется статическими классами (надеюсь, такой термин вообще существует).

Пример простого модуля:

module Example {
    function init (){
        // ...
    }
}

Функция «init» будет вызвана при запуске скрипта (ну, об этом попозже).

А так выглядят два модуля, если один из них используется другим:

module First {
    export function test (){
        console.log ('Эта строчка будет выведена третьей.');
    }

    function init (){
        console.log ('Эта — первой.');
    }
}

module Second {
    import First;

    function init (){
        console.log ('Ну а эта — второй, так как вначале будут проинициализированы все модули, включённые в этот.');
        First.test ();
    }
}


Если поставить перед именем импортируемого класса «@», импортируемый класс может быть проинициализирован и позже импортирующего. Помогает, если каждому из двух классов вдруг понадобится импортировать другой, хотя, конечно же, архитектурно такая структура выглядит отталкивающе.

Ну а ещё есть доступные лишь на чтение экспортируемые переменные:
module First {
    import @Second;

    export function test (){
        console.log ('Тут сейчас будет написано «1024»: ' + Second.value);
    }
}

module Second {
    import First;

    export var value = 1024;

    function init (){
        First.test ();
    }
}


Кстати, модули необязательно использовать именно другими модулями. Хотя и желательно. :)

5. Параметры препроцессора.


Чтобы настроить сборку кода, можно использовать хитрые комментарии в начале файла:
// ==Jsx==
// @target     web:onload
// @build-to   build/
// @define     __VERSION__ 1.00
// @import     utils/string.format
// @include     jquery.min.js
// ==/Jsx==

(Примерно в таком же стиле записываются комментарии для UserScript'ов.)

  • target — указывает, где будет выполняться скрипт. От этого аргумента зависит код, инициализирующий модули. Допустимые значения: «web» (будет использоваться переменная window), «web:onload» (модули начнут инициализироваться после завершения загрузки страницы), «node» (для Node.JS, будет использоваться GLOBAL), «local».
  • build-to — указывает, куда сохранить результат. По умолчанию скрипт сохраняет его в ту же папку, заменив расширение «jsx» на «js» (или добавив расширение «js», если у оригинального файла расширение отличается от «jsx».
  • define — заменит в результирующем файле все вхождения первого аргумента на второй. Иногда пригождается.
  • import — включит файл с расширением «jsxi» (которое писать в параметре не нужно), предварительно распарсив. Если файл не будет найден в папке рядом, поиск продолжится в папке с подключаемыми файлами препроцессора. (Ну, там довольно хитрый алгоритм; пожалуй, не стоит тут расписывать такие мелочи.)
  • include — включит файл в результат, никак не анализируя и не парся.


Это не все параметры, а так же не все фичи, но, по-моему, статья и так получилась чересчур великовата для краткого описания.

Скачать текущую версию можно отсюда. Заранее прошу прощения за все возможные ошибки и благодарю за уделённое внимание. :)

Комментарии 14

    +2
    Дефолтные значения аргументов и многострочные литералы — это, бесспорно, здорово, но пока синтаксис не подсвечивается — лично у меня интерес будет больше академический.

    var chars = Array.prototype.forEach.call ('0123456789abcdef', lambda arg.charCodeAt (0));

    Я бы предпочел какую-нибудь изящную синтаксическую конструкцию для .bind и списочные выражения.
      0
      Добавить подсветку ключевых слов или многострочных строк в тот же Sublime Text несложно, но до полноценной подсветки (список аргументов, например) это, конечно, не дотягивает.

      Синтаксическая конструкция для .bind — отличная идея, надо будет только сперва придумать, как она должна выглядеть.

      А насчёт списочных выражений, можно было бы сделать, например, так:

      var chars = [ '0123456789abcdef' to lambda arg.charCodeAt (0) ]

      Что разворачивалось бы во что-то вроде этого:

      (function (array, fn){
      	var result = [], temp; 
      	for (var i = 0; i < array.length; i ++){
      		temp = fn (array [i]);
      		if (temp !== undefined)
      			result.push (temp);
      	}
      })('0123456789abcdef', function (arg){ return arg.charCodeAt (0) })
      


      Стоит ли делать?
        0
        Синтаксическая конструкция для .bind — отличная идея, надо будет только сперва придумать, как она должна выглядеть.

        Не знаю, посмотрите на Haskell.

        var chars = [ '0123456789abcdef' to lambda arg.charCodeAt (0) ]

        Имхо, скорее, так:
        [lambda arg.toString().charCodeAt(0) for each i in '0123456789abcdef']
          0
          А просто
          [i.toString().charCodeAt(0) for i in '0123456789abcdef']
          не выйдет?

          Получится почти python.
            0
            Да, так еще лучше.
      +4
      Вы видели CoffeeScript?
        +3
        Да, он, конечно, получше, но есть у него один фатальный недостаток...

        Мне полюбился обычный синтаксис JavaScript и, к тому же, не хотелось переписывать уже имеющийся проект. Хотя, конечно, в результате я потратил куда больше времени. Зато инвайт получил. :)
          +2
          Ну а смысл тогда этих lamda function, если есть, к примеру, arrow function? И аргументы функций по умолчанию тоже есть. Нативный JS сильно шагнул за последнее время, вот только не все движки эти новшества поддерживают. Так зачем же городить новый синтаксис, если вам нравится обычный JS?
        +6
        Тогда посмотрите Typescript, он ES6-совместим, да собственно и сам ES6 не за горами (а в ноде с 0.11 с флагом --harmony — уже).

        Хотя опыт написания своего препроцессора со своей версией блекджека в любом случае останется полезным :)
          0
          Флаг --harmony мало что даёт, на данный момент лучшая нативная поддержка будущего стандарта (причём без всяких флагов) у ФФ.
            0
            А про typescript могу сказать, что его использование, как транслятора ECMAScript6 (по личному опыту) весьма сомнительно, т.к. всё равно придётся юзать генерики, интерфейсы и прочие специфичные штуки, а потом замучаемся их удалять, так что если нужна поддержка ECMAScript 6, то лучше сразу взять транслятор именно его.
            0
            А PEG грамматику стандартного javascript вы сами писали или где-то взяли?
              0
              Нет, взял отсюда. Сложно было бы написать самому — я и понятия не имел о том, что за зверь такой эта PEG.
              +3
              Зачем придумывать свой синтаксис если есть ECMAScript 6. Для себя на коленке накатал github.com/kobezzza/NeJS (чужие трансляторы юзал, но бесило, что () => заменяются на .bind и многие трансляторы не позволяют сохранить комментарии).

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

              Самое читаемое