Автор оригинала: 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
— Абдур-Рахман Джанхангир