Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Own Mustache

Own Mustache

Techtalk on implementation of template languages. First was presented on rannts#16 meetup: https://rannts.ru/meetups/16/

Sergey Arkhipov

May 20, 2017
Tweet

More Decks by Sergey Arkhipov

Other Decks in Programming

Transcript

  1. Почему доклад такой странный ― Он для тех, кто умеет

    писать регулярные выражения; ― У меня 40 минут, а в Книге дракона 1184 страницы; ― Книгу дракона я не дочитал; ― И тем не менее, я хочу рассказать полную реализацию шаблонизатора; ― Если вы знаете, в чем разница между LR(0) и LR(2), то вам тут будет скучно; ― 40 минут — у меня нет времени, буду лихо, хамски, срезать кучу углов.
  2. Hello {% if name %}{{ name }}{% else %}default{% /if

    %}! Hello default! {"name": ""} Описание языка
  3. Hello {% for names %}{{ item }} {{% /for %}!

    Hello 12! {"names": ["1", "2"]} Описание языка
  4. Hello {{ first_name }}, {% if last_name %} {{ last_name

    }} {% elif title %} {{ title }} {% else %} Doe {% /if %}! Here is the list of stuff I like: {% loop stuff %} - {{ item.key }} (because of {{ item.value }}) {% /loop %} And that is all!
  5. Анатомия шаблона Hello world {{ name }} {% if something

    %} {% elif condition %} {% else %} {% /if %}
  6. Анатомия шаблона Hello world {{ name }} {% if something

    %} {% elif condition %} {% else %} {% /if %} Block Tags Print Tag Literal Function Expression
  7. Пишем лексер Hello {%if title%}{{title}}{%/if%} {{name}}! 1. Конечный автомат в

    явном виде 2. Составное регулярное выражение
  8. Пишем лексер Hello {%if title%}{{title}}{%/if%} {{name}}! match_start = 6 match_end

    = 17 previous_match_end = 0 text='Hello ' func='if' expr=' title '
  9. Пишем лексер Hello {%if title%}{{title}}{%/if%} {{name}}! match_start = 18 match_end

    = 26 previous_match_end = 17 text='Hello ' func='if' expr=' title ' expr='title'
  10. Пишем лексер Hello {%if title%}{{title}}{%/if%} {{name}}! match_start = 27 match_end

    = 33 previous_match_end = 26 text='Hello ' func='if' expr=' title ' expr='title' func='if'
  11. Пишем лексер Hello {%if title%}{{title}}{%/if%} {{name}}! match_start = 35 match_end

    = 42 previous_match_end = 33 text='Hello ' func='if' expr=' title ' expr='title' func='if' text=' ' expr='name'
  12. Пишем лексер Hello {%if title%}{{title}}{%/if%} {{name}}! match_start = 35 match_end

    = 42 previous_match_end = 33 text='Hello ' func='if' expr=' title ' expr='title' func='if' text=' ' expr='name' text='!'
  13. Пишем лексер def tokenize(text): previous_end = 0 tokens = get_token_patterns()

    if isinstance(text, bytes): text = text.decode("utf-8") for matcher in make_tokenizer_regexp().finditer(text): if matcher.start(0) != previous_end: yield LiteralToken(text[previous_end:matcher.start(0)]) previous_end = matcher.end(0) match_groups = matcher.groupdict() token_class = tokens[matcher.lastgroup] yield token_class(match_groups[matcher.lastgroup]) leftover = text[previous_end:] if leftover: yield LiteralToken(leftover)
  14. Пара слов о парсерах 1. Будем писать парсер по старинке;

    2. Парсер — почти классический shift-reduce (восходящий, rightmost); 3. Мы не будем писать грамматику; 4. Мы будем сразу строить AST на стеке.
  15. Пример грамматики: JSON object → '{' pairs '}' pairs →

    pair pairs_tail | ε pair → STRING ':' value pairs_tail → ',' pairs | ε value → STRING | NUMBER | 'true' | 'false' | 'null' | object | array array → '[' elements ']' elements → value elements_tail | ε elements_tail → ',' elements | ε
  16. Пишем парсер Hello {%if title%}{{title}}{%else%}Mr.{%/if%} {{name}}! text='Hello ' func='if' expr='

    title ' expr='title' func='if' text=' ' expr='name' text='!' func='else' text='Mr.'
  17. Пишем парсер Hello {%if title%}{{title}}{%else%}Mr.{%/if%} {{name}}! func='if' expr=' title '

    expr='title' func='if' text=' ' expr='name' text='!' func='else' text='Mr.' text='Hello ' text('Hello ')
  18. Пишем парсер Hello {%if title%}{{title}}{%else%}Mr.{%/if%} {{name}}! func='if' expr=' title '

    expr='title' func='if' text=' ' expr='name' text='!' func='else' text='Mr.' text='Hello ' text('Hello ') conditional
  19. Пишем парсер Hello {%if title%}{{title}}{%else%}Mr.{%/if%} {{name}}! expr='title' func='if' text=' '

    expr='name' text='!' func='else' text='Mr.' text='Hello ' text('Hello ') conditional if(' title ')
  20. Пишем парсер Hello {%if title%}{{title}}{%else%}Mr.{%/if%} {{name}}! func='if' text=' ' expr='name'

    text='!' func='else' text='Mr.' text='Hello ' text('Hello ') conditional if(' title ') print('title')
  21. Пишем парсер Hello {%if title%}{{title}}{%else%}Mr.{%/if%} {{name}}! func='if' text=' ' expr='name'

    text='!' func='else' text='Mr.' text='Hello ' text('Hello ') conditional if(' title ') print('title')
  22. Пишем парсер Hello {%if title%}{{title}}{%else%}Mr.{%/if%} {{name}}! func='if' text=' ' expr='name'

    text='!' func='else' text='Mr.' text='Hello ' text('Hello ') conditional if(' title ') print('title')
  23. Пишем парсер Hello {%if title%}{{title}}{%else%}Mr.{%/if%} {{name}}! func='if' text=' ' expr='name'

    text='!' text='Mr.' text='Hello ' text('Hello ') conditional if(' title ') print('title') else
  24. Пишем парсер Hello {%if title%}{{title}}{%else%}Mr.{%/if%} {{name}}! func='if' text=' ' expr='name'

    text='!' text='Hello ' text('Hello ') conditional if(' title ') print('title') else text('Mr.')
  25. Пишем парсер Hello {%if title%}{{title}}{%else%}Mr.{%/if%} {{name}}! func='if' text=' ' expr='name'

    text='!' text='Hello ' text('Hello ') conditional if(' title ') print('title') else text('Mr.')
  26. Пишем парсер Hello {%if title%}{{title}}{%else%}Mr.{%/if%} {{name}}! func='if' text=' ' expr='name'

    text='!' text='Hello ' text('Hello ') conditional if(' title ') print('title') else text('Mr.')
  27. Пишем парсер Hello {%if title%}{{title}}{%else%}Mr.{%/if%} {{name}}! func='if' text=' ' expr='name'

    text='!' text='Hello ' text('Hello ') conditional if(' title ') print('title') else text('Mr.')
  28. Пишем парсер Hello {%if title%}{{title}}{%else%}Mr.{%/if%} {{name}}! func='if' text=' ' expr='name'

    text='!' text='Hello ' text('Hello ') conditional if(' title ') print('title') else text('Mr.')
  29. Пишем парсер Hello {%if title%}{{title}}{%else%}Mr.{%/if%} {{name}}! text=' ' expr='name' text='!'

    text='Hello ' text('Hello ') if(' title ') print('title') else text('Mr.')
  30. Пишем парсер Hello {%if title%}{{title}}{%else%}Mr.{%/if%} {{name}}! text='Hello ' text('Hello ')

    if(' title ') print('title') else text('Mr.') text(' ') print('name') Text('!')
  31. Пишем парсер Hello {%if title%}{{title}}{%else%}Mr.{%/if%} {{name}}! text('Hello ') if(' title

    ') print('title') else text('Mr.') text(' ') print('name') Text('!') Root
  32. Пишем парсер Hello {%if title%}{{title}}{%else%}Mr.{%/if%} {{name}}! text('Hello ') if(' title

    ') print('title') else text('Mr.') text(' ') print('name') text('!') Root
  33. Пишем парсер def parse(tokens): stack = [] for token in

    tokens: if isinstance(token, lexer.LiteralToken): stack = parse_literal_token(stack, token) elif isinstance(token, lexer.PrintToken): stack = parse_print_token(stack, token) elif isinstance(token, lexer.StartBlockToken): stack = parse_start_block_token(stack, token) elif isinstance(token, lexer.EndBlockToken): stack = parse_end_block_token(stack, token) else: raise exceptions.CurlyParserUnknownTokenError(token) root = RootNode(stack) validate_for_all_nodes_done(root) return root
  34. Пишем парсер def parse_print_token(stack, token): stack.append(PrintNode(token)) return stack def parse_start_elif_token(stack,

    token): stack = rewind_stack_for(stack, search_for=IfNode) stack.append(IfNode(token)) return stack
  35. Пишем парсер def rewind_stack_for(stack, *, search_for): nodes = [] node

    = None while stack: node = stack.pop() if not node.done: break nodes.append(node) else: raise exceptions.CurlyParserNoUnfinishedNodeError() if not isinstance(node, search_for): raise exceptions.CurlyParserUnexpectedUnfinishedNodeError( search_for, node) node.done = True node.data = nodes[::-1] stack.append(node) return stack
  36. Генерируем шаблон 1. Делаем in-order обход дерева; 2. На основе

    контекста генерируем кусочек текста из ноды; 3. Собираем кусочки в том порядке, в котором они были сгенерированы; 4. Конкатенируем эти кусочки.
  37. Генерируем шаблон class Node: ... def process(self, context): return "".join(self.emit(context))

    def emit(self, context): for node in self: yield from node.emit(context)
  38. Генерируем шаблон class IfNode(BlockTagNode): ... def emit(self, context): if self.evaluate_expression(context):

    yield from super().emit(context) elif self.elsenode: yield from self.elsenode.emit(context)
  39. Генерируем шаблон class LoopNode(BlockTagNode): ... def emit(self, context): resolved =

    self.evaluate_expression(context) context_copy = context.copy() if isinstance(resolved, dict): for key, value in sorted(resolved.items()): context_copy["item"] = {"key": key, "value": value} yield from super().emit(context_copy) else: for item in resolved: context_copy["item"] = item yield from super().emit(context_copy)