Own Mustache

Own Mustache

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

A8d8ca813a744866b9f85ea1cefb5813?s=128

Sergey Arkhipov

May 20, 2017
Tweet

Transcript

  1. 3.

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

    писать регулярные выражения; ― У меня 40 минут, а в Книге дракона 1184 страницы; ― Книгу дракона я не дочитал; ― И тем не менее, я хочу рассказать полную реализацию шаблонизатора; ― Если вы знаете, в чем разница между LR(0) и LR(2), то вам тут будет скучно; ― 40 минут — у меня нет времени, буду лихо, хамски, срезать кучу углов.
  2. 4.
  3. 8.

    Hello {% if name %}{{ name }}{% else %}default{% /if

    %}! Hello default! {"name": ""} Описание языка
  4. 9.

    Hello {% for names %}{{ item }} {{% /for %}!

    Hello 12! {"names": ["1", "2"]} Описание языка
  5. 10.

    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!
  6. 13.

    Анатомия шаблона Hello world {{ name }} {% if something

    %} {% elif condition %} {% else %} {% /if %}
  7. 14.

    Анатомия шаблона Hello world {{ name }} {% if something

    %} {% elif condition %} {% else %} {% /if %} Block Tags Print Tag Literal Function Expression
  8. 15.

    Пишем лексер Hello {%if title%}{{title}}{%/if%} {{name}}! 1. Конечный автомат в

    явном виде 2. Составное регулярное выражение
  9. 26.

    Пишем лексер Hello {%if title%}{{title}}{%/if%} {{name}}! match_start = 6 match_end

    = 17 previous_match_end = 0 text='Hello ' func='if' expr=' title '
  10. 27.

    Пишем лексер Hello {%if title%}{{title}}{%/if%} {{name}}! match_start = 18 match_end

    = 26 previous_match_end = 17 text='Hello ' func='if' expr=' title ' expr='title'
  11. 28.

    Пишем лексер 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'
  12. 29.

    Пишем лексер 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'
  13. 30.

    Пишем лексер 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='!'
  14. 31.
  15. 32.

    Пишем лексер 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)
  16. 33.

    Пара слов о парсерах 1. Будем писать парсер по старинке;

    2. Парсер — почти классический shift-reduce (восходящий, rightmost); 3. Мы не будем писать грамматику; 4. Мы будем сразу строить AST на стеке.
  17. 34.

    Пример грамматики: 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 | ε
  18. 35.

    Пишем парсер 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.'
  19. 36.

    Пишем парсер 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 ')
  20. 37.

    Пишем парсер 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
  21. 38.

    Пишем парсер 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 ')
  22. 39.

    Пишем парсер 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. 40.

    Пишем парсер 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')
  24. 41.

    Пишем парсер 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')
  25. 42.

    Пишем парсер 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
  26. 43.

    Пишем парсер 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. 44.

    Пишем парсер 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. 45.

    Пишем парсер 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. 46.

    Пишем парсер 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.')
  30. 47.

    Пишем парсер 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.')
  31. 48.

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

    text='Hello ' text('Hello ') if(' title ') print('title') else text('Mr.')
  32. 49.
  33. 50.
  34. 51.

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

    if(' title ') print('title') else text('Mr.') text(' ') print('name') Text('!')
  35. 52.

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

    ') print('title') else text('Mr.') text(' ') print('name') Text('!') Root
  36. 53.

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

    ') print('title') else text('Mr.') text(' ') print('name') text('!') Root
  37. 54.
  38. 55.

    Пишем парсер 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
  39. 56.

    Пишем парсер 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
  40. 57.

    Пишем парсер 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
  41. 58.

    Генерируем шаблон 1. Делаем in-order обход дерева; 2. На основе

    контекста генерируем кусочек текста из ноды; 3. Собираем кусочки в том порядке, в котором они были сгенерированы; 4. Конкатенируем эти кусочки.
  42. 59.

    Генерируем шаблон class Node: ... def process(self, context): return "".join(self.emit(context))

    def emit(self, context): for node in self: yield from node.emit(context)
  43. 62.

    Генерируем шаблон 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)
  44. 63.

    Генерируем шаблон 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)