Рубрики
Без рубрики

Как построить предварительный процессор CSS (например, SASS) с нуля

Предварительные обработки CSS очаровывают, соблазняют и завораживают веб-разработчики, но … недолго!. Tagged с Python, Compiler, ShowDev, CSS.

Если вы находитесь в веб -разработке, возможно, вы слышали о Sass , Меньше , Мопс, Стилус и т.п. Все это предварительные обработки. В этом уроке мы собираемся построить не что иное, как функциональный препроцессор CSS с нуля с переменными и функциями. Этот тип нового языка называется Source-To-Source. Если вы взволнованы, я надеюсь не разочаровать вас.

Сначала посмотрим, что мы будем делать. Мы будем называть языковую точку с расширением.

При разработке нового языка хорошо тратить иногда на дизайн. Если у вас есть что -то, что действительно течет, вы можете начать кодирование.

Вот фрагмент дотдота:

x = 1
y = 5
x-y-z = 6
col-z = berry

@f{
    border: 10px solid black;
    border-radius: 50%;
}

.add{
    color:white;
    background-color: rgb(3, 4, 5);
}

#ad{
    color: rgb(x, 4, 5);
}

.a div a{
    color: ..col-z;
    @f
}

который собирается для этого:

.add{
    color: white;
    background-color: rgb(3,4,5);
}

#ad{
    color: rgb(1,4,5);
}

.a div a{
    color: berry;
    border: 10px solid black;
    border-radius: 50%;
}

Посмотрим, какие функции мы включили

Переменные

x = 1
y = 5
x-y-z = 6
col-z = berry

Мы видим, что

  • Нам не нужно указывать спецификатор, такой как $ или var в JS.
  • Мы можем добавить – в имени переменной

Мы также можем вызвать переменные в значениях функции

rgb(x, 4, 5);

И в значениях атрибутов

color: ..col-z;

Где .. обозначает переменный вызов в атрибуте напрямую.

Функции

@f{
    border: 10px solid black;
    border-radius: 50%;
}

Функции обозначены знаком @. Они называются как в следующем случае, когда он расширяется в свойства, которые он содержит.

.a div a{
    color: ..col-z;
    @f
}

Этого достаточно, чтобы справиться. Давайте начнем!

import copy # we have just one import

и мы поместили источник в качестве переменной

source = '''

x = 1
y = 5
x-y-z = 6
col-z = berry

@f{
    border: 10px solid black;
    border-radius: 50%;
}

.add{
    color:white;
    background-color: rgb(3, 4, 5);
}

#ad{
    color: rgb(x, 4, 5);
}

.a div a{
    color: ..col-z;
    @f
}
'''

Мы объединили наши константы в классе.

class SYM:
    LEFT_BRACE = '{'
    RIGHT_BRACE = '}'
    LEFT_ROUND = '('
    RIGHT_ROUND = ')'
    NEW_LINE = '\n'
    TAB = '    '
    COLON = ':'
    SEMI_COLON = ';'
    SPACE = ' '
    COMMA = ','
    EQUAL = '='
    UNION = '-'
    DOT = '.'
    AT = '@'

Затем мы определяем наши ключевые слова

KEYWORDS = (SYM.LEFT_BRACE, SYM.RIGHT_BRACE, SYM.NEW_LINE, SYM.TAB, SYM.COLON, 
    SYM.SEMI_COLON, SYM.SPACE, SYM.RIGHT_ROUND, SYM.LEFT_ROUND, SYM.COMMA, SYM.EQUAL)

Строительство Лексера

Код для Lexer от этот учебник Lexer . Пожалуйста, пройдите через это, если вам нужно. Здесь мы преобразовали код в класс с помощью метода get_lexeme. Метод дает нам список. Приведенный выше фрагмент Dotdot преобразуется в список, хранящийся в переменной Lexemes ниже.

Вот наш класс Lexer:

class Lexer:
    def __init__(self, source: str, KEYWORDS: list):
        self.white_space = SYM.SPACE
        self.KEYWORDS = KEYWORDS
        self.lexeme = ''
        self.lexemes = []
        self.string = source

    def get_lexemes(self) -> list:
        for i, char in enumerate(self.string):
            if char != self.white_space and char != SYM.NEW_LINE:
                self.lexeme += char  # adding a char each time
            if (i+1 < len(self.string)):  # prevents error
                if self.string[i+1] == self.white_space or self.string[i+1] in KEYWORDS or self.lexeme in KEYWORDS or self.string[i+1] == SYM.NEW_LINE: # if next char == ' '
                    if self.lexeme != '':
                        self.lexemes.append(self.lexeme)
                        self.lexeme = ''
        return self.lexemes

Затем мы объявляем наши основные переменные:

v = Lexer(source, KEYWORDS)
lexemes = v.get_lexemes()

lexemes.append('') # appending an unused last element to properly dump all values

Lexemes теперь равен

['x', '=', '1', 'y', '=', '5', 'x-y-z', '=', '6', 'col-z', 
'=', 'berry', '@f', '{', 'border', ':', '10px', 'solid', 
'black', ';', 'border-radius', ':', '50%', ';', '}', '.add', 
'{', 'color', ':', 'white', ';', 'background-color', ':', 
'rgb', '(', '3', ',', '4', ',', '5', ')', ';', '}', '#ad', 
'{', 'color', ':', 'rgb','(', 'x', ',', '4', ',', '5', ')', 
';', '}', '.a', 'div', 'a', '{', 'color', ':', '..col-z', 
';', '@f', '}', '']

С такими разделенными данными гораздо проще продолжить!

Затем мы определяем словарь, чтобы удерживать все наши переменные.

memory = {}

куда

x = 1

станет этим позже:

{'x':'1'}

Чтобы получить это, мы просто сделаем

memory['x']

Концепция дерева

У нас будет словарь под названием Tree

tree = {}

который будет удерживать конвертированный код как

{
    '@f': {
        'border': '10px solid black', 
        'border-radius': '50%'
    }, 
    '.add': {
        'color':'white', 
        'background-color': 'rgb ( 3 , 4 , 5 )'
    }, '#ad': {
        'color': 'rgb ( x ,4 , 5 )'
    }, 
    '.a div a': {
        'color': '..col-z', 
        '@f': ''
    }
}

Наш следующий шаг будет именно таким: преобразование списка Lexemes в этот словарь

Чтобы отслеживать, где мы находимся, у нас будет ряд переменных, принимающих истинные/ложные значения (включено/выключение)

Настройка держателей

id_string = ''
last_id_string = ''
last_attribute = ''

current_attribute = ''
current_value = ''

len_lexemes = len(lexemes)

Идентификационная строка будет держать как #Add, .x Div и т. Д.

Переменные последнего_ просто хранят переменные, которые не опустошены после выхода из подразделения

Настройка флагов

У нас будет 3 флаги.

Один, когда мы начинаем блок, который станет истинным при столкновении с {и false, когда встречается a}

Атрибут – цвет в цвете: черный;

Постоянный атрибут станет верным при прохождении {и; и станет ложным при прохождении:

value_ongoing станет истинной при переходе: и ложь при переходе;

block_ongoing = False
attribute_ongoing = False
value_ongoing = False

Мы начнем зацикливаться на списке и реализовать то, что мы описали в вышеупомянутых флагах

for i, lex in enumerate(lexemes):

    if i+1 < len_lexemes:
        next_lexeme = lexemes[i+1]
    prev_lexeme = lexemes[i-1]

    if lex == SYM.LEFT_BRACE:
        block_ongoing = True
        attribute_ongoing = True
        continue
    elif lex == SYM.RIGHT_BRACE:
        block_ongoing = False
        statement_ongoing = False
        attribute_ongoing = False
        value_ongoing = False
        continue
    if lex == SYM.COLON:
        value_ongoing = True
        attribute_ongoing = False
        continue
    elif lex == SYM.SEMI_COLON:
        value_ongoing = False
        statement_ongoing = False
        attribute_ongoing = True
        continue

Продолжение необходимо, так как, как только мы активировали флаг, у нас нет работы, мы идем дальше.

Чтобы назначить переменные, мы просто ждем, затем продолжимся.

Затем, переходя через имя переменной или значение, мы просто предотвращаем продолжение цикла, используя продолжение

    if lex == SYM.EQUAL:
        memory[prev_lexeme] = next_lexeme
        continue
    elif next_lexeme == SYM.EQUAL or prev_lexeme == SYM.EQUAL:
        continue

Теперь мы воспользуемся флагами

    if not block_ongoing:
        id_string += lex + ' '
    elif block_ongoing:
        if id_string:
            tree[id_string.strip()] = {}
            last_id_string = id_string.strip()
            id_string = ''

Здесь мы имеем дело с идентификатором блока, пример #Add в #ADD {}. Мы сделали

tree[id_string.strip()] = {}

Вот пример дерева на этом этапе

{
'.add': {}
}

Используя тот же принцип, мы сделаем это для атрибута

    if attribute_ongoing:
        current_attribute += lex
    elif not attribute_ongoing:
        if current_attribute:
            tree[last_id_string][current_attribute] = ''
            last_attribute = current_attribute
            current_attribute = ''

Вот пример дерева на этом этапе

{
'.add': {
        'color':''
    }
}

и значение

    if value_ongoing:
        current_value += lex + ' '
    elif not value_ongoing:
        if current_value:
            tree[last_id_string][last_attribute] = current_value.strip()
            last_value = current_value.strip()
            current_value = ''

Вот пример дерева на этом этапе

{
'.add': {
        'color':'white'
    }
}

Этот фрагмент заканчивает наш блок построения деревьев

Теперь давайте посмотрим, как заменить имена переменных в таких функциях, как rgb () и rgba ()

def parseFunc(f):
    v = f.split(SYM.LEFT_ROUND)
    name = v[0]
    vals = v[1][:-1].replace(SYM.SPACE, '')
    values = vals.split(SYM.COMMA)
    for i, v in enumerate(values):
        if not v.isnumeric():
            values[i] = memory[v]
    return '{}({})'.format(name.strip(), ','.join(values))

Здесь мы заменяем RGB (x, 4, 5) на RGB (1, 4, 5), заменив x на 1 в словаре памяти. Чтобы построить препроцессор CSS с нуля, мы должны взять все дела, что мы сделаем после.

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

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

ntree = copy.deepcopy(tree)

Тогда мы перечислим:

for block_name in ntree:
    properties = ntree[block_name]
    if block_name[0] == SYM.AT:
        continue

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

    for element in properties:
        value = properties[element]
        if SYM.LEFT_ROUND in value:
            tree[block_name][element] = parseFunc(value)

Затем мы говорим, есть ли (в значении, это обозначает функцию, такую как rgb (). В этом случае мы используем нашу функцию parsefunc.

        if SYM.DOT in value:
            tree[block_name][element] = memory[value.strip(SYM.DOT)]

Далее мы видим. В значении мы посмотрим в словаре памяти и заменим его

        if SYM.AT in element:
            del tree[block_name][element]
            tree[block_name].update(tree[element])

Затем мы видим символ @ как ключ в блоке, мы просто добавляем в него словаря в словаря в этом блоке (выполненном Update () в Python).

Это также показывает преимущество использования структуры данных по простым строкам.

Теперь мы можем скомпилировать его в обычный CSS, мы просто распечатаем его на данный момент (вы можете распечатать кстати)

for key in tree:
    if key[0] == SYM.AT:
        continue
    print(key, '{', sep='')
    for elem in tree[key]:
        print('{}{}: {};'.format(SYM.TAB, elem, tree[key][elem]))
    print('}\n')

который производит

.add{
    color: white;
    background-color: rgb(3,4,5);
}

#ad{
    color: rgb(1,4,5);
}

.a div a{
    color: berry;
    border: 10px solid black;
    border-radius: 50%;
}

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

Если вы хотите построить предварительную процедуру CSS с нуля на основе отступления (например, Stylus), используйте этот учебник Анкет

Что следует добавить следующее? Прокомментируйте это ниже!

Ссылка GitHub: https://github.com/Abdur-rahmaanJ/dotdot

Зеркально на Pythonmembersclub

Изображения из Unsplash

-Абдур-Рахмаан Джанхангер

Оригинал: “https://dev.to/abdurrahmaanj/how-to-build-a-css-pre-processor-like-sass-from-scratch-33mc”