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

Рассказ о том, как написать свой собственный CSS препроцессор за 9 месяцев

Время на прочтение 22 мин
Количество просмотров 28K
Xочу рассказать о своем детище – препроцессоре и парсере CSS, которым я начал заниматься с апреля прошлого года. Зачем я начал заниматься им? Признаваясь себе честно уже сейчас, я могу сказать: хотелось изобрести свой собственный велосипед. Чем я руководствовался тогда? Трудно сказать. Возможно, тем же самым. А возможно, тем, что я толком не нашел ничего удовлетворяющего моим требованиям к CSS препроцессору для моей любимой платформы разработки.

Требования к CSS препроцессору у меня сформировались после прочтения одной из статей здесь. Это была статья про препроцессор «Stylus для Node.js». Собственно, тогда то я про эти «препроцессоры» и узнал. Меня поразила вся простота синтаксиса этого препроцессора. После двухдневного (а может и меньшего) просмотра результатов с гугла, я ничего интересного для себя не нашел. Вот именно в этот момент ко мне в голову и пришла шальная мысль: а почему бы нет?
Требования у меня были следующие:
  • Наиболее простой синтаксис (ну это само собой!)
  • Язык разработки – PHP (ну теперь то уже можно и сказать)
  • Возможность отображения исходного файла стилей в виде дерева блоков

Собственно, наверно, это и все.

Что у меня получилось?


Все это время, прошедшее с начальной точки отсчета, я потратил на разработку своего продукта, и у меня получилось следующее — библиотека MySheet с открытым исходным кодом, являющаяся одновременно и парсером, и препроцессором стилей CSS.

Ну, конечно же, мне пришлось написать небольшой сайтик для презентации своего продукта (вы уже догадались, какой язык программирования был выбран мной для этой цели?). Несколько интересных моментов процесса разработки – о том, как я создавал свою библиотеку, с какими подводными камнями столкнулся и что я узнал в процессе ее создания – я расскажу чуть позже. А теперь, о возможностях моего CSS препроцессора.
  • Арифметические выражения
    Данная возможность есть практически в каждом CSS препроцессоре. Не обошла она стороной и библиотеку MySheet:

    $wrapper_height = 50%
    .wrapper
        height $wrapper_height + 20px
        top ($wrapper_height / 2)
    

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

    .object
        color #a50c5b - 50sat /* decrease saturation by 50% */
        background-color #a50c5b + 50lt /* make color lighter by 50 percent */
    

    Да! Это именно то, о чем вы сейчас думаете! Можно выполнять арифметические действия над цветами! Я надеюсь, вы вдоволь поиграетесь с этой фишкой на официальном сайте библиотеки, а теперь перейдем к следующему пункту в нашем списке.
  • Mixin’ы
    Ну, вот, не смог я написать это слово на русском. Уж больно многие пишут его на английском:

    @mixin filter-grayscale(percent)
        -webkit-filter: grayscale($percent);
        -ms-filter: grayscale($percent);
        -o-filter: grayscale($percent);
        filter: grayscale($percent);
    
    img
        filter-grayscale 100%
    img:hover
        filter-grayscale 0%
    

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

    В объявлении миксина также доступна переменная $arguments, которая просто перечислит все переданные аргументы в одну строку:

    @mixin border-radius(topleft, topright)
        -webkit-border-radius $topleft $topright 4px 5px
        border-radius $arguments
    

    Я постарался, чтобы в препроцессор были уже встроены некоторые миксины. Это, во-первых, благоприятно скажется на производительности. Во-вторых, позволит разработчикам и дизайнерам сосредоточиться на написании стилей для сайта, а не методов для упрощения этого процесса. Плюс, второе часто бывает делать лень.
  • Функции
    Функций в библиотеки пока что всего – три. Это – abs, negate и unitless. Но база для их написания подготовлена, и в будущем список доступных функций будет расширяться.

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

    html
        height 0
        width 50px !prefixWith(ms, moz) !important
        border-radius 5px !important
        filter-grayscale 50%
        transform scale(2)
    

    Флаг в данном случае применяется для удобного добавления префиксов к правилам и компилируется в следующий код CSS:

    html {
        height: 0;
        -ms-width: 50px !important;
        -moz-width: 50px !important;
        width: 50px !important;
        -moz-border-radius: 5px;
        -webkit-border-radius: 5px;
        border-radius: 5px;
        -webkit-filter: grayscale(50%);
        filter: grayscale(50%);
        -ms-transform: scale(2);
        -moz-transform: scale(2);
        -o-transform: scale(2);
        -webkit-transform: scale(2);
        transform: scale(2)
    }
    

    Флаг выполняет некоторые действия над конкретным правилом. У меня есть идея использовать флаги еще и для того, чтобы помечать и наделять правила определенными свойствами. Например, так можно сделать флаг !noMixin, который запретит компиляцию и вставку миксина в код CSS. Таким образом можно избежать расширения синтаксиса лишними символами и ключевыми словами.
  • Плагины
    Я старался сделать свою библиотеку расширяемой. Т.е. чтобы любой человек (и Вы, и я, и, вообще, любая домохозяйка) могли расширить возможности библиотеки написанием плагина, а не созданием форка на гитхаб и переворачиванием всего исходного кода (хотя второе я и не воспрещаю делать). Сейчас написано два плагина для библиотеки MySheet:
    — PluginMixin — добавляет возможность использования миксинов в коде MSS (MySheet Styles)
    — PluginSelectorExtensions — добавляет вкусняшки вроде: обращение к родительскому селектору (или группе селекторов) через символ & и псевдо-селектор :any(), который я нагло содрал с препроцессора CSSCrush.
  • Дерево блоков
    Это, собственно, и есть третье по списку требование, о котором я писал в начале статьи. Я хотел, чтобы стилями CSS можно было управлять не только непосредственно, изменяя исходники вручную, но и делать это из бэкэнда, т.е. производить те манипуляции с кодом, которые присущи CSS парсерам (тык и еще тык). Что это дает? Например, можно изменять стили сайта на основании предпочтений пользователя. А предпочтения могут быть самыми разными. Одни хотят шрифт больше, другие – передвинуть заголовок на главной страницы немного ниже. Данную возможность можно добавлять к различным генераторам сайтов, что благоприятно скажется на аудитории пользователей программного продукта.

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

    html { color red; text-align: center; margin: 0 auto; }
    @mixin rounded-corners (top, right, bottom, left)
        -webkit-border-radius \$left + \$right \$top + \$bottom
        -moz-border-radius \$arguments
        border-radius \$arguments \$left \$right \$left \$right
            
    @mixin diagonal-border-radius(left, right)
        border-radius \$arguments \$right \$left
            
    @page 
        padding 5px
    body
        rounded-corners 1 2 3 4 
        .wrapper
            diagonal-border-radius 6px 10px
            h1 span
                color blue
    

    Пропустим этот код через парсер:

    <?php
    try {
        $result = $mysheet->parseCode($code); 
        $compiledCode = $result->toRealCss();
    } catch (\MSSLib\Error\MySheetException $ex) {
        echo($ex->getTraceAsString());
    }
    

    и на выходе получим примерно следующее дерево:
    Дерево блоков MSS
    object(MSSLib\Structure\Document)[64]
      protected '_docFilePath' => null
      protected 'children' => 
        array (size=5)
          0 => 
            object(MSSLib\Structure\Ruleset)[98]
              private '_selectors' => 
                array (size=1)
                  0 => 
                    object(MSSLib\Structure\Selector)[99]
                      private '_mssPath' => string 'html' (length=4)
                      private '_cssPathGroup' => 
                        object(MSSLib\Structure\CssSelectorGroup)[180]
                          private 'paths' => 
                            array (size=1)
                              0 => string 'html' (length=4)
                      private '_ruleset' => 
                        &object(MSSLib\Structure\Ruleset)[98]
                      private '_isFullSelector' => null
                      private '_isParsed' => boolean true
                      private '_handlerMap' => null
              protected '_parentRuleset' => null
              protected 'children' => 
                array (size=3)
                  0 => 
                    object(MSSLib\Structure\Declaration)[100]
                      private 'ruleName' => string 'color' (length=5)
                      private 'ruleValue' => 
                        object(MSSLib\Structure\RuleValue)[101]
                          private 'params' => 
                            array (size=1)
                              0 => 
                                object(MSSLib\EmbeddedClasses\ColorClass)[104]
                                  protected 'type' => string 'html' (length=4)
                                  protected 'color' => 
                                    array (size=1)
                                      0 => string 'red' (length=3)
                                  protected '_colorLib' => null
                          private '_parentDeclaration' => 
                            &object(MSSLib\Structure\Declaration)[100]
                          private '_flags' => 
                            array (size=0)
                              empty
                      private 'ruleEnabled' => boolean true
                      private 'parent' (MSSLib\Structure\Block) => null
                      private '_handlerMap' => null
                      private '_handlerMap' (MSSLib\Structure\Block) => null
                  1 => 
                    object(MSSLib\Structure\Declaration)[102]
                      private 'ruleName' => string 'text-align' (length=10)
                      private 'ruleValue' => 
                        object(MSSLib\Structure\RuleValue)[103]
                          private 'params' => 
                            array (size=1)
                              0 => 
                                object(MSSLib\EmbeddedClasses\NonQuotedStringClass)[107]
                                  protected 'text' => string 'center' (length=6)
                          private '_parentDeclaration' => 
                            &object(MSSLib\Structure\Declaration)[102]
                          private '_flags' => 
                            array (size=0)
                              empty
                      private 'ruleEnabled' => boolean true
                      private 'parent' (MSSLib\Structure\Block) => null
                      private '_handlerMap' => null
                      private '_handlerMap' (MSSLib\Structure\Block) => null
                  2 => 
                    object(MSSLib\Structure\Declaration)[105]
                      private 'ruleName' => string 'margin' (length=6)
                      private 'ruleValue' => 
                        object(MSSLib\Structure\RuleValue)[106]
                          private 'params' => 
                            array (size=2)
                              0 => 
                                object(MSSLib\EmbeddedClasses\MetricClass)[110]
                                  protected 'metric' => float 0
                                  protected 'unit' => null
                              1 => 
                                object(MSSLib\EmbeddedClasses\NonQuotedStringClass)[111]
                                  protected 'text' => string 'auto' (length=4)
                          private '_parentDeclaration' => 
                            &object(MSSLib\Structure\Declaration)[105]
                          private '_flags' => 
                            array (size=0)
                              empty
                      private 'ruleEnabled' => boolean true
                      private 'parent' (MSSLib\Structure\Block) => null
                      private '_handlerMap' => null
                      private '_handlerMap' (MSSLib\Structure\Block) => null
              private 'parent' (MSSLib\Structure\Block) => 
                &object(MSSLib\Structure\Document)[64]
              private '_handlerMap' (MSSLib\Structure\Block) => null
          1 => 
            object(MSSLib\Plugins\Mixin\Mixin)[97]
              protected 'name' => string 'rounded-corners' (length=15)
              protected 'locals' => 
                array (size=4)
                  0 => string 'top' (length=3)
                  1 => string 'right' (length=5)
                  2 => string 'bottom' (length=6)
                  3 => string 'left' (length=4)
              protected 'children' => 
                array (size=3)
                  0 => 
                    object(MSSLib\Structure\Declaration)[109]
                      private 'ruleName' => string '-webkit-border-radius' (length=21)
                      private 'ruleValue' => 
                        object(MSSLib\Structure\RuleValue)[113]
                          private 'params' => 
                            array (size=2)
                              0 => 
                                object(MSSLib\EmbeddedClasses\MathExprClass)[122]
                                  protected 'expressionTree' => 
                                    object(MSSLib\Essentials\ExpressionTree\ExpressionNode)[116]
                                      private 'value' (Tree\Node\Node) => null
                                      private 'parent' (Tree\Node\Node) => null
                                      private 'children' (Tree\Node\Node) => 
                                        array (size=3)
                                          0 => 
                                            object(MSSLib\Essentials\ExpressionTree\ParamNode)[117]
                                              private 'value' (Tree\Node\Node) => 
                                                object(MSSLib\EmbeddedClasses\VariableClass)[119]
                                                  private 'varName' => string 'left' (length=4)
                                              private 'parent' (Tree\Node\Node) => 
                                                &object(MSSLib\Essentials\ExpressionTree\ExpressionNode)[116]
                                              private 'children' (Tree\Node\Node) => 
                                                array (size=0)
                                                  empty
                                          1 => 
                                            object(MSSLib\Essentials\ExpressionTree\OperatorNode)[118]
                                              private 'value' (Tree\Node\Node) => 
                                                object(MSSLib\Operators\PlusOperator)[120]
                                              private 'parent' (Tree\Node\Node) => 
                                                &object(MSSLib\Essentials\ExpressionTree\ExpressionNode)[116]
                                              private 'children' (Tree\Node\Node) => 
                                                array (size=0)
                                                  empty
                                          2 => 
                                            object(MSSLib\Essentials\ExpressionTree\ParamNode)[121]
                                              private 'value' (Tree\Node\Node) => 
                                                object(MSSLib\EmbeddedClasses\VariableClass)[123]
                                                  private 'varName' => string 'right' (length=5)
                                              private 'parent' (Tree\Node\Node) => 
                                                &object(MSSLib\Essentials\ExpressionTree\ExpressionNode)[116]
                                              private 'children' (Tree\Node\Node) => 
                                                array (size=0)
                                                  empty
                              1 => 
                                object(MSSLib\EmbeddedClasses\MathExprClass)[130]
                                  protected 'expressionTree' => 
                                    object(MSSLib\Essentials\ExpressionTree\ExpressionNode)[124]
                                      private 'value' (Tree\Node\Node) => null
                                      private 'parent' (Tree\Node\Node) => null
                                      private 'children' (Tree\Node\Node) => 
                                        array (size=3)
                                          0 => 
                                            object(MSSLib\Essentials\ExpressionTree\ParamNode)[125]
                                              private 'value' (Tree\Node\Node) => 
                                                object(MSSLib\EmbeddedClasses\VariableClass)[127]
                                                  private 'varName' => string 'top' (length=3)
                                              private 'parent' (Tree\Node\Node) => 
                                                &object(MSSLib\Essentials\ExpressionTree\ExpressionNode)[124]
                                              private 'children' (Tree\Node\Node) => 
                                                array (size=0)
                                                  empty
                                          1 => 
                                            object(MSSLib\Essentials\ExpressionTree\OperatorNode)[126]
                                              private 'value' (Tree\Node\Node) => 
                                                object(MSSLib\Operators\PlusOperator)[128]
                                              private 'parent' (Tree\Node\Node) => 
                                                &object(MSSLib\Essentials\ExpressionTree\ExpressionNode)[124]
                                              private 'children' (Tree\Node\Node) => 
                                                array (size=0)
                                                  empty
                                          2 => 
                                            object(MSSLib\Essentials\ExpressionTree\ParamNode)[129]
                                              private 'value' (Tree\Node\Node) => 
                                                object(MSSLib\EmbeddedClasses\VariableClass)[131]
                                                  private 'varName' => string 'bottom' (length=6)
                                              private 'parent' (Tree\Node\Node) => 
                                                &object(MSSLib\Essentials\ExpressionTree\ExpressionNode)[124]
                                              private 'children' (Tree\Node\Node) => 
                                                array (size=0)
                                                  empty
                          private '_parentDeclaration' => 
                            &object(MSSLib\Structure\Declaration)[109]
                          private '_flags' => 
                            array (size=0)
                              empty
                      private 'ruleEnabled' => boolean true
                      private 'parent' (MSSLib\Structure\Block) => null
                      private '_handlerMap' => null
                      private '_handlerMap' (MSSLib\Structure\Block) => null
                  1 => 
                    object(MSSLib\Structure\Declaration)[114]
                      private 'ruleName' => string '-moz-border-radius' (length=18)
                      private 'ruleValue' => 
                        object(MSSLib\Structure\RuleValue)[115]
                          private 'params' => 
                            array (size=1)
                              0 => 
                                object(MSSLib\EmbeddedClasses\VariableClass)[134]
                                  private 'varName' => string 'arguments' (length=9)
                          private '_parentDeclaration' => 
                            &object(MSSLib\Structure\Declaration)[114]
                          private '_flags' => 
                            array (size=0)
                              empty
                      private 'ruleEnabled' => boolean true
                      private 'parent' (MSSLib\Structure\Block) => null
                      private '_handlerMap' => null
                      private '_handlerMap' (MSSLib\Structure\Block) => null
                  2 => 
                    object(MSSLib\Structure\Declaration)[132]
                      private 'ruleName' => string 'border-radius' (length=13)
                      private 'ruleValue' => 
                        object(MSSLib\Structure\RuleValue)[133]
                          private 'params' => 
                            array (size=5)
                              0 => 
                                object(MSSLib\EmbeddedClasses\VariableClass)[137]
                                  private 'varName' => string 'arguments' (length=9)
                              1 => 
                                object(MSSLib\EmbeddedClasses\VariableClass)[138]
                                  private 'varName' => string 'left' (length=4)
                              2 => 
                                object(MSSLib\EmbeddedClasses\VariableClass)[139]
                                  private 'varName' => string 'right' (length=5)
                              3 => 
                                object(MSSLib\EmbeddedClasses\VariableClass)[140]
                                  private 'varName' => string 'left' (length=4)
                              4 => 
                                object(MSSLib\EmbeddedClasses\VariableClass)[141]
                                  private 'varName' => string 'right' (length=5)
                          private '_parentDeclaration' => 
                            &object(MSSLib\Structure\Declaration)[132]
                          private '_flags' => 
                            array (size=0)
                              empty
                      private 'ruleEnabled' => boolean true
                      private 'parent' (MSSLib\Structure\Block) => null
                      private '_handlerMap' => null
                      private '_handlerMap' (MSSLib\Structure\Block) => null
              private 'parent' (MSSLib\Structure\Block) => 
                &object(MSSLib\Structure\Document)[64]
              private '_handlerMap' (MSSLib\Structure\Block) => null
              protected 'plugin' => 
                object(MSSLib\Plugins\Mixin\PluginMixin)[58]
                  private '_registeredMixins' => 
                    array (size=0)
                      empty
                  private '_systemMixins' => 
                    array (size=3)
                      'border-radius' => 
                        array (size=2)
                          0 => 
                            object(MSSLib\Plugins\Mixin\EmbeddedMixins\BasicSet)[61]
                          1 => string 'border_radius' (length=13)
                      'transform' => 
                        array (size=2)
                          0 => 
                            object(MSSLib\Plugins\Mixin\EmbeddedMixins\BasicSet)[61]
                          1 => string 'transform' (length=9)
                      'filter-grayscale' => 
                        array (size=2)
                          0 => 
                            object(MSSLib\Plugins\Mixin\EmbeddedMixins\BasicSet)[61]
                          1 => string 'filter_grayscale' (length=16)
                  protected '_enabledMixinSetClasses' => 
                    array (size=1)
                      0 => string 'basic' (length=5)
          2 => 
            object(MSSLib\Plugins\Mixin\Mixin)[112]
              protected 'name' => string 'diagonal-border-radius' (length=22)
              protected 'locals' => 
                array (size=2)
                  0 => string 'left' (length=4)
                  1 => string 'right' (length=5)
              protected 'children' => 
                array (size=1)
                  0 => 
                    object(MSSLib\Structure\Declaration)[136]
                      private 'ruleName' => string 'border-radius' (length=13)
                      private 'ruleValue' => 
                        object(MSSLib\Structure\RuleValue)[142]
                          private 'params' => 
                            array (size=3)
                              0 => 
                                object(MSSLib\EmbeddedClasses\VariableClass)[145]
                                  private 'varName' => string 'arguments' (length=9)
                              1 => 
                                object(MSSLib\EmbeddedClasses\VariableClass)[146]
                                  private 'varName' => string 'right' (length=5)
                              2 => 
                                object(MSSLib\EmbeddedClasses\VariableClass)[147]
                                  private 'varName' => string 'left' (length=4)
                          private '_parentDeclaration' => 
                            &object(MSSLib\Structure\Declaration)[136]
                          private '_flags' => 
                            array (size=0)
                              empty
                      private 'ruleEnabled' => boolean true
                      private 'parent' (MSSLib\Structure\Block) => null
                      private '_handlerMap' => null
                      private '_handlerMap' (MSSLib\Structure\Block) => null
              private 'parent' (MSSLib\Structure\Block) => 
                &object(MSSLib\Structure\Document)[64]
              private '_handlerMap' (MSSLib\Structure\Block) => null
              protected 'plugin' => 
                object(MSSLib\Plugins\Mixin\PluginMixin)[58]
                  private '_registeredMixins' => 
                    array (size=0)
                      empty
                  private '_systemMixins' => 
                    array (size=3)
                      'border-radius' => 
                        array (size=2)
                          0 => 
                            object(MSSLib\Plugins\Mixin\EmbeddedMixins\BasicSet)[61]
                          1 => string 'border_radius' (length=13)
                      'transform' => 
                        array (size=2)
                          0 => 
                            object(MSSLib\Plugins\Mixin\EmbeddedMixins\BasicSet)[61]
                          1 => string 'transform' (length=9)
                      'filter-grayscale' => 
                        array (size=2)
                          0 => 
                            object(MSSLib\Plugins\Mixin\EmbeddedMixins\BasicSet)[61]
                          1 => string 'filter_grayscale' (length=16)
                  protected '_enabledMixinSetClasses' => 
                    array (size=1)
                      0 => string 'basic' (length=5)
          3 => 
            object(MSSLib\Structure\AtRule)[135]
              protected '_name' => string 'page' (length=4)
              protected '_parameters' => string '' (length=0)
              protected 'children' => 
                array (size=1)
                  0 => 
                    object(MSSLib\Structure\Declaration)[143]
                      private 'ruleName' => string 'padding' (length=7)
                      private 'ruleValue' => 
                        object(MSSLib\Structure\RuleValue)[148]
                          private 'params' => 
                            array (size=1)
                              0 => 
                                object(MSSLib\EmbeddedClasses\MetricClass)[151]
                                  protected 'metric' => float 5
                                  protected 'unit' => string 'px' (length=2)
                          private '_parentDeclaration' => 
                            &object(MSSLib\Structure\Declaration)[143]
                          private '_flags' => 
                            array (size=0)
                              empty
                      private 'ruleEnabled' => boolean true
                      private 'parent' (MSSLib\Structure\Block) => 
                        &object(MSSLib\Structure\AtRule)[135]
                      private '_handlerMap' => null
                      private '_handlerMap' (MSSLib\Structure\Block) => null
              private 'parent' (MSSLib\Structure\Block) => 
                &object(MSSLib\Structure\Document)[64]
              private '_handlerMap' (MSSLib\Structure\Block) => null
          4 => 
            object(MSSLib\Structure\Ruleset)[144]
              private '_selectors' => 
                array (size=1)
                  0 => 
                    object(MSSLib\Structure\Selector)[150]
                      private '_mssPath' => string 'body' (length=4)
                      private '_cssPathGroup' => 
                        object(MSSLib\Structure\CssSelectorGroup)[96]
                          private 'paths' => 
                            array (size=1)
                              0 => string 'body' (length=4)
                      private '_ruleset' => 
                        &object(MSSLib\Structure\Ruleset)[144]
                      private '_isFullSelector' => null
                      private '_isParsed' => boolean true
                      private '_handlerMap' => null
              protected '_parentRuleset' => null
              protected 'children' => 
                array (size=3)
                  0 => 
                    object(MSSLib\Structure\Declaration)[152]
                      private 'ruleName' => string 'rounded-corners' (length=15)
                      private 'ruleValue' => 
                        object(MSSLib\Structure\RuleValue)[153]
                          private 'params' => 
                            array (size=4)
                              0 => 
                                object(MSSLib\EmbeddedClasses\MetricClass)[156]
                                  protected 'metric' => float 1
                                  protected 'unit' => null
                              1 => 
                                object(MSSLib\EmbeddedClasses\MetricClass)[157]
                                  protected 'metric' => float 2
                                  protected 'unit' => null
                              2 => 
                                object(MSSLib\EmbeddedClasses\MetricClass)[158]
                                  protected 'metric' => float 3
                                  protected 'unit' => null
                              3 => 
                                object(MSSLib\EmbeddedClasses\MetricClass)[159]
                                  protected 'metric' => float 4
                                  protected 'unit' => null
                          private '_parentDeclaration' => 
                            &object(MSSLib\Structure\Declaration)[152]
                          private '_flags' => 
                            array (size=0)
                              empty
                      private 'ruleEnabled' => boolean true
                      private 'parent' (MSSLib\Structure\Block) => null
                      private '_handlerMap' => null
                      private '_handlerMap' (MSSLib\Structure\Block) => null
                  1 => 
                    object(MSSLib\Structure\Declaration)[154]
                      private 'ruleName' => string 'transform' (length=9)
                      private 'ruleValue' => 
                        object(MSSLib\Structure\RuleValue)[155]
                          private 'params' => 
                            array (size=1)
                              0 => 
                                object(MSSLib\EmbeddedClasses\FunctionClass)[167]
                                  protected 'name' => string 'rotate' (length=6)
                                  protected 'arguments' => 
                                    array (size=1)
                                      0 => 
                                        object(MSSLib\EmbeddedClasses\MathExprClass)[174]
                                          protected 'expressionTree' => 
                                            object(MSSLib\Essentials\ExpressionTree\ExpressionNode)[170]
                                              private 'value' (Tree\Node\Node) => null
                                              private 'parent' (Tree\Node\Node) => null
                                              private 'children' (Tree\Node\Node) => 
                                                array (size=2)
                                                  0 => 
                                                    object(MSSLib\Essentials\ExpressionTree\OperatorNode)[171]
                                                      private 'value' (Tree\Node\Node) => 
                                                        object(MSSLib\Operators\UnaryMinusOperator)[172]
                                                      private 'parent' (Tree\Node\Node) => 
                                                        &object(MSSLib\Essentials\ExpressionTree\ExpressionNode)[170]
                                                      private 'children' (Tree\Node\Node) => 
                                                        array (size=0)
                                                          empty
                                                  1 => 
                                                    object(MSSLib\Essentials\ExpressionTree\ParamNode)[173]
                                                      private 'value' (Tree\Node\Node) => 
                                                        object(MSSLib\EmbeddedClasses\MetricClass)[175]
                                                          protected 'metric' => float 5
                                                          protected 'unit' => string 'deg' (length=3)
                                                      private 'parent' (Tree\Node\Node) => 
                                                        &object(MSSLib\Essentials\ExpressionTree\ExpressionNode)[170]
                                                      private 'children' (Tree\Node\Node) => 
                                                        array (size=0)
                                                          empty
                                  protected '_functionRenderer' => 
                                    object(MSSLib\Essentials\FunctionRenderers\DefaultFunctionRenderer)[166]
                          private '_parentDeclaration' => 
                            &object(MSSLib\Structure\Declaration)[154]
                          private '_flags' => 
                            array (size=0)
                              empty
                      private 'ruleEnabled' => boolean true
                      private 'parent' (MSSLib\Structure\Block) => null
                      private '_handlerMap' => null
                      private '_handlerMap' (MSSLib\Structure\Block) => null
                  2 => 
                    object(MSSLib\Structure\Ruleset)[149]
                      private '_selectors' => 
                        array (size=1)
                          0 => 
                            object(MSSLib\Structure\Selector)[161]
                              private '_mssPath' => string '.wrapper' (length=8)
                              private '_cssPathGroup' => 
                                object(MSSLib\Structure\CssSelectorGroup)[176]
                                  private 'paths' => 
                                    array (size=1)
                                      0 => string 'body .wrapper' (length=13)
                              private '_ruleset' => 
                                &object(MSSLib\Structure\Ruleset)[149]
                              private '_isFullSelector' => null
                              private '_isParsed' => boolean true
                              private '_handlerMap' => null
                      protected '_parentRuleset' => 
                        &object(MSSLib\Structure\Ruleset)[144]
                      protected 'children' => 
                        array (size=2)
                          0 => 
                            object(MSSLib\Structure\Declaration)[168]
                              private 'ruleName' => string 'diagonal-border-radius' (length=22)
                              private 'ruleValue' => 
                                object(MSSLib\Structure\RuleValue)[169]
                                  private 'params' => 
                                    array (size=2)
                                      0 => 
                                        object(MSSLib\EmbeddedClasses\MetricClass)[178]
                                          protected 'metric' => float 6
                                          protected 'unit' => string 'px' (length=2)
                                      1 => 
                                        object(MSSLib\EmbeddedClasses\MetricClass)[179]
                                          protected 'metric' => float 10
                                          protected 'unit' => string 'px' (length=2)
                                  private '_parentDeclaration' => 
                                    &object(MSSLib\Structure\Declaration)[168]
                                  private '_flags' => 
                                    array (size=0)
                                      empty
                              private 'ruleEnabled' => boolean true
                              private 'parent' (MSSLib\Structure\Block) => null
                              private '_handlerMap' => null
                              private '_handlerMap' (MSSLib\Structure\Block) => null
                          1 => 
                            object(MSSLib\Structure\Ruleset)[164]
                              private '_selectors' => 
                                array (size=1)
                                  0 => 
                                    object(MSSLib\Structure\Selector)[177]
                                      private '_mssPath' => string 'h1 span' (length=7)
                                      private '_cssPathGroup' => 
                                        object(MSSLib\Structure\CssSelectorGroup)[183]
                                          private 'paths' => 
                                            array (size=1)
                                              0 => string 'body .wrapper h1 span' (length=21)
                                      private '_ruleset' => 
                                        &object(MSSLib\Structure\Ruleset)[164]
                                      private '_isFullSelector' => null
                                      private '_isParsed' => boolean true
                                      private '_handlerMap' => null
                              protected '_parentRuleset' => 
                                &object(MSSLib\Structure\Ruleset)[149]
                              protected 'children' => 
                                array (size=1)
                                  0 => 
                                    object(MSSLib\Structure\Declaration)[181]
                                      private 'ruleName' => string 'color' (length=5)
                                      private 'ruleValue' => 
                                        object(MSSLib\Structure\RuleValue)[182]
                                          private 'params' => 
                                            array (size=1)
                                              0 => 
                                                object(MSSLib\EmbeddedClasses\ColorClass)[185]
                                                  protected 'type' => string 'html' (length=4)
                                                  protected 'color' => 
                                                    array (size=1)
                                                      0 => string 'blue' (length=4)
                                                  protected '_colorLib' => null
                                          private '_parentDeclaration' => 
                                            &object(MSSLib\Structure\Declaration)[181]
                                          private '_flags' => 
                                            array (size=0)
                                              empty
                                      private 'ruleEnabled' => boolean true
                                      private 'parent' (MSSLib\Structure\Block) => null
                                      private '_handlerMap' => null
                                      private '_handlerMap' (MSSLib\Structure\Block) => null
                              private 'parent' (MSSLib\Structure\Block) => 
                                &object(MSSLib\Structure\Ruleset)[149]
                              private '_handlerMap' (MSSLib\Structure\Block) => null
                      private 'parent' (MSSLib\Structure\Block) => 
                        &object(MSSLib\Structure\Ruleset)[144]
                      private '_handlerMap' (MSSLib\Structure\Block) => null
              private 'parent' (MSSLib\Structure\Block) => 
                &object(MSSLib\Structure\Document)[64]
              private '_handlerMap' (MSSLib\Structure\Block) => null
      private 'parent' (MSSLib\Structure\Block) => null
      private '_handlerMap' (MSSLib\Structure\Block) => null
    


    Что в конечном итоге компилируется в следующий CSS-код:
    html {
        color: #ff0000;
        text-align: center;
        margin: 0 auto
    }
    
    @page  {
        padding: 5px
    }
    
    body {
        -webkit-border-radius: 6 4;
        -moz-border-radius: 1 2 3 4;
        border-radius: 1 2 3 4 4 2 4 2;
        -ms-transform: rotate(-5deg);
        -moz-transform: rotate(-5deg);
        -o-transform: rotate(-5deg);
        -webkit-transform: rotate(-5deg);
        transform: rotate(-5deg)
    }
    
    body .wrapper {
        border-radius: 6px 10px 10px 6px
    }
    
    body .wrapper h1 span {
        color: #0000ff
    }
    

  • Формат вывода CSS
    Форматом выходного CSS-кода можно управлять с помощью настроек библиотеки. Для этого следует задать предпочтительные префиксы и суффиксы к строкам:

    $mysheet = MySheet::Instance();
    $mysheet->setActiveDirectory(realpath('./'));
    $mysheet->getAutoload()->registerAutoload();
    $settings = new MSSettings();
    $settings->set('cssRenderer', [
        'prefixRule' => '   ',
        'suffixRule' => ' /* this is a real CSS rule */',
        'sepSelectors' => ', ',
        'sepRules' => '; ',
        'prefixOCB' => ' ',
        'suffixOCB' => "\n",
        'prefixCCB' => "\n",
        'suffixCCB' => ''
    ]);
    …
    $mysheet->init($settings);
    

    Приведу таблицу всех возможных префиксов и суффиксов:
    Название Значение по-умолчанию Описание
    prefixRule 4 пробела Строка, вставляемая перед каждым правилом
    suffixRule Пустая строка Строка, вставляемая после каждого правила
    sepSelectors , Разделитель между селекторами
    sepRules ;\n Разделитель между правилами
    prefixOCB Пробел Строка, вставляемая перед открывающейся фигурной скобкой (OCB – opening curly bracket)
    suffixOCB \n Строка, вставляемая после открывающейся фигурной скобки
    prefixСCB \n Строка, вставляемая перед закрывающейся фигурной скобкой (CCB – closing curly bracket)
    suffixCCB \n Строка, вставляемая после закрывающейся фигурной скобки
    prefixAtRuleLine 4 пробела Строка, вставляемая перед каждой строкой внутри @-правила
    suffixAtRuleLine Пустая строка Строка, вставляемая после каждой строки внутри @-правила

  • Другие возможности
    В библиотеке есть и другие возможности и функции, которые я просто перечислю списком. К ним относятся:
    — Включение и отключение компиляции правила (через символ ~, добавляемый перед правилом)
    — Автоматическое встраивание мелких изображений в код CSS с помощью data: URL
    — Импортирование других MSS и CSS файлов с помощью директивы @ import (над этой возможностью я еще работаю; в частности, нужно добавить возможность задания опций импортирования)
    — Преобразование всех цветов к одному формату
    — 2 поддерживаемых языка для текста ошибок компиляции: английский (en_us) и русский (ru_ru)
    — Включение и отключение расширений парсера библиотеки (можно отключить поддержку функций, переменных, цветов и т.п.)

Как это было…


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


Рис. 1 – Моя любимая девчонка

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

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

Название для своей библиотеки я придумывал, действуя от обратного. Я решил не отходить от примера самых известных на сегодняшний день препроцессоров SASS и LESS, и подумал, что MSS – неплохое сочетание букв, в конце концов. А чтобы название было запоминающимся, я решил назвать свой проект MySheet. И в аббревиатуру укладывается (MySheet Styles), и лёгкая изюминка в названии есть.

Название придумано, пора начинать проектировать корабль. Первое, что я начал делать – это был парсер исходного кода. Вот тут-то я и просчитался в первый раз. Я начал делать его без разбития исходного кода на токены и ключевые слова, и ориентировался на то, что в библиотеке будет фигурировать в-основном работа со строками. Конечно, минусы этого подхода я видел уже на этапе его выбора, но ничего лучшего, к сожалению, я придумать не смог. Уже потом, во время того как в университете у нас читался курс по компиляторам, я понял, что лучше было бы ввести этап предварительного лексического анализа. Хотя бы, потому что сейчас я столкнулся с проблемой распознавания и запоминания комментариев для последующего их вывода в скомпилированный код CSS. Или, например, теперь мне бы хотелось добиться нечувствительности расширений парсера (которые подключаются к библиотеки в виде дополнительных модулей) к наличию нежданных переносов строк и отступов (тех самых, которые часто добавляются для удобочитаемости кода). В ближайшем будущем, я хочу включить в парсер этап разбития на токены, что, по моему мнению, должно разрешить эти проблемы.

Идея с арифметикой цветов мне пришла в голову, когда я реализовывал поддержку математических выражений. Я подумал, что неплохо было бы иметь возможность осветления и затемнения цветов в дизайне сайта, чтобы подобрать сочетающийся цвет можно было без необходимости открытия color picker’а. Сейчас в библиотеке реализована работа с HSLA, RGBA, HEX и HTML форматами цветов. К каждому из цветов, заданных в данных форматах, можно добавить дельту любого канала из какого-либо другого формата цвета. Например, к цвету записанному как #000 можно добавить 255 пунктов синего канала и 40 пунктов зеленого, получив при этом цвет #0028ff. Арифметическое выражение будет выглядеть в данном случае следующим образом: #000 + 255b + 40g.

Реализовывая работу с цветами, я решил не изобретать свой велосипед и использовать уже существующую библиотеку MrColor (хотя без «допиливания» этой библиотеки не обошлось).

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

Хочу рассказать одну интересную штуку. Когда парсер в препроцессоре начинает парсить какое-либо правило, он проходит по всем зарегистрированным модулям и, грубо говоря, вызывает в каждом из них метод parse. А так как арифметическое выражение и, например, функция – две разные сущности, то библиотеке приходилось парсить одно и то же два раза. Мне это очень не нравилось, и в один прекрасный день я придумал решение. Когда парсер арифметического выражения обнаруживает, что перед ним все ж таки никакое не выражение, а простая функция, он не возвращает false, а возвращает этот самый объект функции. Тем самым я избавился от этого изъяна и увеличил производительность процесса парсинга исходного кода.

Еще хочу рассказать, как я делал свой собственный первый логотип. В поисках идеи для логотипа, я набрел на картинку с тюбиками краски, и подумал: «Тысяча чертей! Да это же просто замечательная идея!». Я посмотрел несколько уроков по рисованию в Фотошопе и, в итоге, у меня получилась вот такая клякса:


Рис. 2 – Моя клякса

Мои дальнейшие планы


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

Потом, я хочу пойти в сторону развития функционала по редактированию стилей и поиску блоков в коде MSS из бэкэнда. Например, можно добавить поддержку условных комментариев IE прямо в исходном файле (не знаю как вас, но меня всегда раздражало, что патчи для IE нужно сохранять и включать на страницу в виде отдельных файлов, тем более, если это всего полтора CSS-правила).

Если у кого-то есть какие-то ещё идеи по совершенствованию моего проекта, я всегда буду рад их услышать.

Вместо заключения


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

Буду благодарен вам за возможные советы и рекомендации, а также за всевозможную поддержку и просто теплые слова.

Если вам понравилась моя статья, то я обязательно буду писать ещё.

Ссылки


GitHub: https://github.com/Dobby007/mysheet
Оффициальный сайт: http://mss.flydigo.com/
Документация: http://mss.flydigo.com/docs

P.S. Я извиняюсь, но как кто-то заметил в комментариях — мой хостинг не выдержал напора. Про это я даже и подумать не мог. Сейчас попробуем что-нибудь сделать с этим…

P.P.S. К сожалению, у моего хостинга, оказывается, драконовские ограничения на сайты (причем не только бесплатные). А на запросы они отвечают ровно один раз в китайскую пасху, поэтому придется мне посмотреть в сторону других возможных решений для содержания моего сайта.

P.P.P.S. Доступ к сайту будет полностью восстановлен после обновления всей цепочки DNS. Сейчас сайт доступен по адресу: dobby007_h5a5nu.radius-host.net
Теги:
Хабы:
+22
Комментарии 64
Комментарии Комментарии 64

Публикации

Истории

Работа

PHP программист
175 вакансий

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

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн