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

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

Сасс, Меньше или Меньше? Теперь у вас есть идея, как развернуть свой собственный препроцессор!

Автор оригинала: Abdur-Rahmaan Janhangeer.

Если вы занимаетесь веб-разработкой, возможно, вы слышали о Sass , Less , Pug, Stylus и т. Д. Все это предварительные процессоры. В этом уроке мы собираемся создать не что иное, как функциональный препроцессор css с нуля с переменными и функциями. Этот тип нового языка называется скомпилированным от источника к источнику. Если вы в восторге, я надеюсь, что не разочарую вас.

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

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

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

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)

Построение лексера

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

Вот наш класс лексеров:

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

лексемы теперь равны

['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 = {}

который будет содержать преобразованный код как

{
    '@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': ''
    }
}

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

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

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

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

current_attribute = ''
current_value = ''

len_lexemes = len(lexemes)

строка идентификатора будет содержать такие значения, как #add,. x a div и т. Д

переменные last_ просто содержат переменные, которые не опустошаются при выходе из подраздела

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

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

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

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

Атрибут continuing станет истинным при передаче { и ; и станет ложным при передаче:

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

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

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

Затем при переходе по имени переменной или значению мы просто предотвращаем продолжение цикла с помощью 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 in #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 с нуля на основе отступов (например, стилуса), используйте этот учебник .

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

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

Зеркальное отображение на Клуб участников Python

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

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