$30 off During Our Annual Pro Sale. View Details »

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. Свой Mustache за 40 минут
    Сергей Архипов, 2017

    View Slide

  2. http:/
    /9seconds.github.io/curly

    View Slide

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

    View Slide

  4. View Slide

  5. Stanford CS143
    https://web.stanford.edu/class/archive/cs/cs143/cs143.1128/

    View Slide

  6. Описание языка
    Hello world!
    Hello world!

    View Slide

  7. Hello {{ name }}!
    Hello Jane!
    {"name": "Jane"}
    Описание языка

    View Slide

  8. Hello {% if name %}{{ name }}{%
    else %}default{% /if %}!
    Hello default!
    {"name": ""}
    Описание языка

    View Slide

  9. Hello {% for names %}{{ item }}
    {{% /for %}!
    Hello 12!
    {"names": ["1", "2"]}
    Описание языка

    View Slide

  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!

    View Slide

  11. Лексер
    Мама мыла раму.
    Сущ(Мама), Пробел, Глг(мыла),
    Пробел, Сущ(раму), Пнкт(.)

    View Slide

  12. Парсер
    Сущ(Мама), Пробел, Глг(мыла),
    Пробел, Сущ(раму), Пнкт(.)
    Предложение
    Сущ(Мама)
    Пробел
    Глг(мыла)
    Пробел
    Сущ(раму)
    Пнкт(.)

    View Slide

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

    View Slide

  14. Анатомия шаблона
    Hello world
    {{ name }}
    {% if something %}
    {% elif condition %}
    {% else %}
    {% /if %}
    Block Tags
    Print Tag
    Literal
    Function
    Expression

    View Slide

  15. Пишем лексер
    Hello {%if title%}{{title}}{%/if%} {{name}}!
    1. Конечный автомат в явном виде
    2. Составное регулярное выражение

    View Slide

  16. Пишем лексер
    Function \w+
    Expression (?:\\.|\w)+
    LPrintBracket {{
    RPrintBracket }}
    LBlockBracket {%
    RBlockBracket %}
    Literal .*?

    View Slide

  17. Пишем лексер
    FUNC_RE = r"[a-zA-Z0-9_-]+"
    EXP_RE = r"(?:\\.|[^\{\}%])+"

    View Slide

  18. Пишем лексер
    PRINT_RE = r"""
    {{\s*
    (%s)
    \s*}}
    """ % EXP_RE

    View Slide

  19. Пишем лексер
    START_BLOCK_RE = r"""
    {%%\s*
    (%s)
    \s*
    (%s)?
    \s*%%}
    """ % (FUNC_RE, EXP_RE)

    View Slide

  20. Пишем лексер
    END_BLOCK_RE = r"""
    {%%\s*
    /\s*
    (%s)
    \s*%%}
    """ % FUNC_RE

    View Slide

  21. Пишем лексер
    TOKENIZE_RE = r"(?P<{0}>{1})|(?
    P<{2}>{3})|(?P<{4}>{5})".format(
    "print", PRINT_RE,
    "start_block", START_BLOCK_RE,
    "end_block", END_BLOCK_RE
    )

    View Slide

  22. Пишем лексер
    re.finditer(pattern, string, flags=0)
    re.match.start([group])
    re.match.end([group])
    re.match.lastgroup

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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'

    View Slide

  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'

    View Slide

  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'

    View Slide

  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='!'

    View Slide

  31. Пишем лексер
    Hello {%if title%}{{title}}{%/if%} {{name}}!
    text='Hello '
    func='if' expr=' title '
    expr='title'
    func='if'
    text=' '
    expr='name'
    text='!'

    View Slide

  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)

    View Slide

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

    View Slide

  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 | ε

    View Slide

  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.'

    View Slide

  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 ')

    View Slide

  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

    View Slide

  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 ')

    View Slide

  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')

    View Slide

  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')

    View Slide

  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')

    View Slide

  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

    View Slide

  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.')

    View Slide

  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.')

    View Slide

  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.')

    View Slide

  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.')

    View Slide

  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.')

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  54. View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  58. Генерируем шаблон
    1. Делаем in-order обход дерева;
    2. На основе контекста генерируем кусочек текста из
    ноды;
    3. Собираем кусочки в том порядке, в котором они
    были сгенерированы;
    4. Конкатенируем эти кусочки.

    View Slide

  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)

    View Slide

  60. Генерируем шаблон
    class LiteralNode(Node):
    ...
    @property
    def text(self):
    return self.token.contents["text"]
    def emit(self, _):
    yield self.text

    View Slide

  61. Генерируем шаблон
    class PrintNode(ExpressionMixin, Node):
    ...
    def emit(self, context):
    yield str(self.evaluate_expression(context))

    View Slide

  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)

    View Slide

  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)

    View Slide

  64. Спасибо!
    @9seconds   
    https://speakerdeck.com/9seconds/own-mustache
    https://9seconds.github.io/curly

    View Slide