Notas de Design (pt_BR)

O texto a seguir descreve o conhecimento coletado durante a implementação do rst2html5. Certamente não está completo e talvez nem esteja exato, mas pode ser de grande utilidade para outras pessoas que desejem criar um novo tradutor de rst para algum outro formato.

Note

O módulo rst2html5 teve de ser renomeado para rst2html5_ devido a um conflito com o módulo de mesmo nome do docutils.

Docutils

O Docutils é um conjunto de ferramentas para processamento de documentação em texto simples em marcação restructuredText (rst) para outros formatos tais como HTML, PDF e Latex. Seu funcionamento básico está descrito em http://docutils.sourceforge.net/docs/peps/pep-0258.html

Nas primeiras etapas do processo de tradução, o documento rst é analisado e convertido para um formato intermediário chamado de doctree, que então é passado a um tradutor para ser transformado na saída formatada desejada:

                        Tradutor
                 +-------------------+
                 |    +---------+    |
---> doctree -------->|  Writer |-------> output
                 |    +----+----+    |
                 |         |         |
                 |         |         |
                 |  +------+------+  |
                 |  | NodeVisitor |  |
                 |  +-------------+  |
                 +-------------------+

Doctree

O doctree é uma estrutura hierárquica dos elementos que compõem o documento rst, usada internamente pelos componentes do Docutils. Está definida no módulo docutils.nodes.

O comando/aplicativo rst2pseudoxml.py gera uma representação textual da doctree que é muito útil para visualizar o aninhamento dos elementos de um documento rst. Essa informação foi de grande ajuda tanto para o design quanto para os testes do rst2html5_.

Dado o trecho de texto rst abaixo:

Título
======

Texto e mais texto

A sua representação textual produzida pelo rst2pseudoxml é:

<document ids="titulo" names="título" source="snippet.rst" title="Título">
    <title>
        Título
    <paragraph>
        Texto e mais texto

Tradutor, Writer e NodeVisitor

Um tradutor é formado por duas partes: Writer e NodeVisitor. A responsabilidade do Writer é preparar e coordenar a tradução feita pelo NodeVisitor. O NodeVisitor é responsável por visitar cada nó da doctree e executar a ação necessária de tradução para o formato desejado de acordo com o tipo e conteúdo do nó.

Note

Estas classes correspondem a uma variação do padrão de projeto “Visitor” conhecida como “Extrinsic Visitor” que é mais comumente usada em Python. Veja The “Visitor Pattern”, Revisited.

Important

Para desenvolver um novo tradutor para o docutils, é necessário especializar estas duas classes.

              +-------------+
              |             |
              |    Writer   |
              |  translate  |
              |             |
              +------+------+
                     |
                     |    +---------------------------+
                     |    |                           |
                     v    v                           |
                +------------+                        |
                |            |                        |
                |    Node    |                        |
                |  walkabout |                        |
                |            |                        |
                +--+---+---+-+                        |
                   |   |   |                          |
         +---------+   |   +----------+               |
         |             |              |               |
         v             |              v               |
+----------------+     |    +--------------------+    |
|                |     |    |                    |    |
|  NodeVisitor   |     |    |    NodeVisitor     |    |
| dispatch_visit |     |    | dispatch_departure |    |
|                |     |    |                    |    |
+--------+-------+     |    +---------+----------+    |
         |             |              |               |
         |             +--------------|---------------+
         |                            |
         v                            v
+-----------------+          +------------------+
|                 |          |                  |
|   NodeVisitor   |          |   NodeVisitor    |
|  visit_TIPO_NÓ  |          |  depart_TIPO_NÓ  |
|                 |          |                  |
+-----------------+          +------------------+

During doctree traversal through docutils.nodes.Node.walkabout(), there are two NodeVisitor dispatch methods called: dispatch_visit() and dispatch_departure(). The former is called early in the node visitation. Then, all children nodes walkabout() are visited and lastly the latter dispatch method is called. Each dispatch method calls a specific visit_NODE_TYPE or depart_NODE_TYPE method such as visit_paragraph or depart_title, that should be implemented by the NodeVisitor subclass object.

Durante a travessia da doctree feita através do método docutils.nodes.Node.walkabout(), há dois métodos dispatch de NodeVisitor chamados: dispatch_visit() e dispatch_departure(). O primeiro é chamado logo no começo da visitação do nó. Em seguida, todos os nós-filho são visitados e, por último, o método dispatch_departure é chamado. Cada um desses métodos chama um método cujo nome segue o padrão visit_NODE_TYPE ou depart_NODE_TYPE, tal como visit_paragraph ou depart_title, que deve ser implementado na subclasse de NodeVisitor.

Para a doctree do exemplo anterior, a sequência de chamadas visit_... e depart_... seria:

1. visit_document
    2. visit_title
        3. visit_Text
        4. depart_Text
    5. depart_title
    6. visit_paragraph
        7. visit_Text
        8. depart_Text
    9. depart_paragraph
10. depart_document

Note

São nos métodos visit_... e depart_... onde deve ser feita a tradução de cada nó de acordo com seu tipo e conteúdo.

rst2html5

O módulo rst2html5_ segue as recomendações originais e especializa as classes Writer e NodeVisitor através das classes HTML5Writer e HTML5Translator. rst2html5_.HTML5Translator é a subclasse de NodeVisitor criada para implementar todos os métodos visit_TIPO_NÓ e depart_TIPO_NÓ necessários para traduzir uma doctree em seu correspondente HTML5. Isto é feito com ajuda de um outro objeto da classe auxiliar ElemStack que controla uma pilha de contextos para lidar com o aninhamento da visitação dos nós da doctree e com a endentação:

                   rst2html5
           +-----------------------+
           |    +-------------+    |
doctree ------->| HTML5Writer |------->  HTML5
           |    +------+------+    |
           |           |           |
           |           |           |
           |  +--------+--------+  |
           |  | HTML5Translator |  |
           |  +--------+--------+  |
           |           |           |
           |           |           |
           |     +-----+-----+     |
           |     | ElemStack |     |
           |     +-----------+     |
           +-----------------------+

A ação padrão de um método visit_TIPO_NÓ é iniciar um novo contexto para o nó sendo tratado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    def default_visit(self, node):
        '''
        Initiate a new context to store inner HTML5 elements.
        '''
        if 'ids' in node and self.once_attr('expand_id_to_anchor', default=True):
            # create an anchor <a id=id></a> on top of the current element
            # for each id found.
            for id in node['ids'][1:]:
                self.context.begin_elem()
                self.context.commit_elem(tag.a(id=id))
            node.attributes['ids'] = node.attributes['ids'][0:1]
        self.context.begin_elem()
        return

A ação padrão no depart_TIPO_NÓ é criar o elemento HTML5 de acordo com o contexto salvo:

1
2
3
4
5
6
7
8
9
    def default_departure(self, node):
        '''
        Create the node's corresponding HTML5 element and combine it with its
        stored context.
        '''
        tag_name, indent, attributes = self.parse(node)
        elem = getattr(tag, tag_name)(**attributes)
        self.context.commit_elem(elem, indent)
        return

Nem todos os elementos rst seguem o este processamento. O elemento Text, por exemplo, é um nó folha e, por isso, não requer a criação de um contexto específico. Basta adicionar o texto correspondente ao elemento pai.

Outros tipos de nós têm um processamento comum e podem compartilhar o mesmo método visit_ e/ou depart_. Para aproveitar essas similaridades, é feito um mapeamento entre o nó rst e os métodos correspondentes pelo dicionário rst_terms:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
             'The "<html{html_attr}>" placeholder is recommended.',
                ['--template'],
                {'metavar': '<filename or text>', 'default': None,
                 'dest': 'template',
                 'type': 'string',
                 'action': 'store', }),
            ('Define a case insensitive identifier to be used with ifdef and ifndef directives. '
                'There is no value associated with an identifier. '
                '(This option can be used multiple times)',
                ['--define'],
                {'metavar': '<identifier>',
                 'dest': 'identifiers',
                 'default': None,
                 'action': 'append', }),
        )
    )

    settings_defaults = {
        'tab_width': 4,
        'syntax_highlight': 'short',
        'field_limit': 0,
        'option_limit': 0,
    }

    def __init__(self):
        writers.Writer.__init__(self)
        self.translator_class = HTML5Translator
        return

    def translate(self):
        self.parts['pseudoxml'] = self.document.pformat()  # get pseudoxml before HTML5.translate
        self.document.reporter.debug('%s pseudoxml:\n %s' %
                                     (self.__class__.__name__, self.parts['pseudoxml']))
        visitor = self.translator_class(self.document)
        self.document.walkabout(visitor)
        self.output = visitor.output
        self.head = visitor.head
        self.body = visitor.body
        self.title = visitor.title
        self.docinfo = visitor.docinfo
        return

    def assemble_parts(self):
        writers.Writer.assemble_parts(self)
        self.parts['head'] = self.head
        self.parts['body'] = self.body
        self.parts['title'] = self.title
        self.parts['docinfo'] = self.docinfo
        return

    def get_transforms(self):
        return writers.Writer.get_transforms(self) + [FooterToBottom, WrapTopTitle]


class ElemStack(object):

    '''
    Helper class to handle nested contexts and indentation
    '''

    def __init__(self, settings):
        self.stack = []
        self.indent_level = 0
        self.indent_output = settings.indent_output
        self.indent_width = settings.tab_width

    def _indent_elem(self, element, indent):
        result = []
        # Indentation schema:
        #
        #         current position
        #                |
        #                v
        #           <tag>|
        # |   indent   |<elem>
        # |indent-1|</tag>
        #          ^
        #      ends here
        if self.indent_output and indent:
            indentation = '\n' + self.indent_width * self.indent_level * ' '
            result.append(indentation)
        result.append(element)
        if self.indent_output and indent:
            indentation = '\n' + self.indent_width * (self.indent_level - 1) * ' '
            result.append(indentation)
        return result

    def append(self, element, indent=True):
        '''
        Append to current element
        '''
        self.stack[-1].append(self._indent_elem(element, indent))
        return

    def begin_elem(self):
        '''
        Start a new element context
        '''
        self.stack.append([])
        self.indent_level += 1
        return

    def commit_elem(self, elem, indent=True):
        '''
        A new element is create by removing its stack to make a tag.
        This tag is pushed back into its parent's stack.
        '''
        pop = self.stack.pop()
        elem(*pop)
        self.indent_level -= 1
        self.append(elem, indent)
        return

    def pop(self):
        return self.pop_elements(1)[0]

    def pop_elements(self, num_elements):
        assert num_elements > 0
        parent_stack = self.stack[-1]
        result = []

Construção de Tags HTML5

A construção das tags do HTML5 é feita através do objeto tag do módulo genshi.builder.

Genshi Builder

Support for programmatically generating markup streams from Python code using a very simple syntax. The main entry point to this module is the tag object (which is actually an instance of the ElementFactory class). You should rarely (if ever) need to directly import and use any of the other classes in this module.

Elements can be created using the tag object using attribute access. For example:

>>> doc = tag.p('Some text and ', tag.a('a link', href='http://example.org/'), '.')
>>> doc
<Element "p">

This produces an Element instance which can be further modified to add child nodes and attributes. This is done by “calling” the element: positional arguments are added as child nodes (alternatively, the Element.append method can be used for that purpose), whereas keywords arguments are added as attributes:

>>> doc(tag.br)
<Element "p">
>>> print(doc)
<p>Some text and <a href="http://example.org/">a link</a>.<br/></p>

If an attribute name collides with a Python keyword, simply append an underscore to the name:

>>> doc(class_='intro')
<Element "p">
>>> print(doc)
<p class="intro">Some text and <a href="http://example.org/">a link</a>.<br/></p>

As shown above, an Element can easily be directly rendered to XML text by printing it or using the Python str() function. This is basically a shortcut for converting the Element to a stream and serializing that stream:

>>> stream = doc.generate()
>>> stream #doctest: +ELLIPSIS
<genshi.core.Stream object at ...>
>>> print(stream)
<p class="intro">Some text and <a href="http://example.org/">a link</a>.<br/></p>

The tag object also allows creating “fragments”, which are basically lists of nodes (elements or text) that don’t have a parent element. This can be useful for creating snippets of markup that are attached to a parent element later (for example in a template). Fragments are created by calling the tag object, which returns an object of type Fragment:

>>> fragment = tag('Hello, ', tag.em('world'), '!')
>>> fragment
<Fragment>
>>> print(fragment)
Hello, <em>world</em>!

ElemStack

Como a travessia da doctree não é feita por recursão, é necessária uma estrutura auxiliar de pilha para armazenar os contextos prévios. A classe auxiliar ElemStack é uma pilha que registra os contextos e controla o nível de endentação.

O comportamento do objeto ElemStack é ilustrado a seguir, através da visualização da estrutura de pilha durante a análise do trecho rst que vem sendo usado como exemplo. As chamadas visit_... e depart_... acontecerão na seguinte ordem:

1. visit_document
    2. visit_title
        3. visit_Text
        4. depart_Text
    5. depart_title
    6. visit_paragraph
        7. visit_Text
        8. depart_Text
    9. depart_paragraph
10. depart_document
  1. Estado inicial. A pilha de contexto está vazia:

    context = []
    
  2. visit_document. Um novo contexto para document é criado:

    context = [ [] ]
                 \
                  document
                  context
    
  3. visit_title. Um novo contexto é criado para o elemento title:

                    title
                    context
                     /
    context = [ [], [] ]
                 \
                  document
                  context
    
  4. visit_Text. O nó do tipo Text não precisa de um novo contexto pois é um nó-folha. O texto é simplesmente adicionado ao contexto do seu nó-pai:

                      title
                      context
                     /
    context = [ [], ['Title'] ]
                 \
                  document
                  context
    
  5. depart_Text. Nenhuma ação é executada neste passo. A pilha permanece inalterada.

  6. depart_title. Representa o fim do processamento do título. O contexto do título é extraído da pilha e combinado com uma tag h1 que é inserida no contexto do nó-pai (document context):

    context = [ [tag.h1('Title')] ]
                 \
                  document
                  context
    
  7. visit_paragraph. Um novo contexto é criado:

                                     paragraph
                                     context
                                    /
    context = [ [tag.h1('Title')], [] ]
                 \
                  document
                  context
    
  8. visit_Text. Mais uma vez, o texto é adicionado ao contexto do nó-pai:

                                     paragraph
                                     context
                                    /
    context = [ [tag.h1('Title')], ['Text and more text'] ]
                 \
                  document
                  context
    
  9. depart_Text. Nenhuma ação é necessária.

  10. depart_paragraph. Segue o comportamento padrão, isto é, o contexto é combinado com a tag do elemento rst atual e então é inserida no contexto do nó-pai:

    context = [ [tag.h1('Title'), tag.p('Text and more text')] ]
                 \
                  document
                  context
    
  11. depart_document. O nó da classe document não tem um correspondente em HTML5. Seu contexto é simplesmente combinado com o contexto mais geral que será o body:

context = [tag.h1('Title'), tag.p('Text e more text')]

Testes

Os testes executados no módulo rst2html5_.tests.test_html5writer são baseados em geradores (veja http://nose.readthedocs.org/en/latest/writing_tests.html#test-generators). Os casos de teste são registrados no arquivo tests/cases.py. Cada caso de teste fica registrado em uma variável do tipo dicionário cujas entradas principais são:

rst:Trecho de texto rst a ser transformado
out:Saída esperada
part:A qual parte da saída produzida pelo rst2html5_ será usada na comparação com out. As partes possíveis são: head, body e whole.

Todas as demais entradas são consideradas opções de configuração do rst2html5_. Exemplos: indent_output, script, script-defer, html-tag-attr e stylesheet.

Em caso de falha no teste, três arquivos auxiliares são gravados no diretório temporário (/tmp no Linux):

  1. NOME_CASO_TESTE.rst com o trecho de texto rst do caso de teste;
  2. NOME_CASO_TESTE.result com resultado produzido pelo rst2html5_ e
  3. NOME_CASO_TESTE.expected com o resultado esperado pelo caso de teste.

Em que NOME_CASO_TESTE é o nome da variável que contém o dicionário do caso de teste.

A partir desses arquivos é mais fácil comparar as diferenças:

$ kdiff3 /tmp/NOME_CASO_TESTE.result /tmp/NOME_CASO_TESTE.expected