Source code for hieroglyph.writer

"""Writer Support for Hieroglyph Slides."""

from docutils import nodes
from sphinx.locale import _
from docutils.writers.html4css1 import HTMLTranslator as BaseTranslator
from sphinx.writers.html import HTMLTranslator

from hieroglyph import html
from hieroglyph.directives import (
    slide,
    slideconf,
)


def depart_title(self, node):

    # XXX Because we want to inject our link into the title, this is
    # largely copy-pasta'd from sphinx.html.writers.HtmlTranslator.

    close_tag = self.context[-1]

    if (self.permalink_text and self.builder.add_permalinks and
            node.parent.hasattr('ids') and node.parent['ids']):
        aname = node.parent['ids'][0]

        if close_tag.startswith('</a></h'):
            self.body.append('</a>')

        self.body.append(u'<a class="headerlink" href="#%s" ' % aname +
                         u'title="%s">%s</a>' % (
                         _('Permalink to this headline'),
                         self.permalink_text))

        self.body.append(
            u'<a class="headerlink" href="%s#%s" ' % (
                html.slide_path(self.builder),
                aname,
            ) +
            u'title="%s">%s' % (
                _('Slides'),
                self.builder.app.config.slide_html_slide_link_symbol,
            ))

        if not close_tag.startswith('</a></h'):
            self.body.append('</a>')

    BaseTranslator.depart_title(self, node)


class SlideData(object):

    def __init__(self, translator, **kwargs):

        self._translator = translator

        self.level = 0
        self.title = ''
        self.content = ''
        self.classes = []
        self.slide_number = 0
        self.id = ''

        for name, value in kwargs.items():
            setattr(self, name, value)

    def _filter_classes(self, include=None, exclude=None):

        classes = self.classes[:]
        if include is not None:
            classes = [
                c[len(include):] for c in classes
                if c.startswith(include)
            ]

        if exclude is not None:
            classes = [
                c for c in classes
                if not c.startswith(exclude)
            ]

        return classes

    def get_slide_context(self):
        """Return the context dict for rendering this slide."""

        return {
            'title': self.title,
            'level': self.level,
            'content': self.content,
            'classes': self.classes,
            'slide_classes': self._filter_classes(exclude='content-'),
            'content_classes': self._filter_classes(include='content-'),
            'slide_number': self.slide_number,
            'config': self._translator.builder.config,
            'id': self.id,
        }


class BaseSlideTranslator(HTMLTranslator):

    def __init__(self, *args, **kwargs):

        HTMLTranslator.__init__(self, *args, **kwargs)

        self.section_count = 0
        self.body_stack = []
        self.current_slide = None
        self.slide_data = []

    def push_body(self):
        """Push the current body onto the stack and create an empty one."""

        self.body_stack.append(self.body)
        self.body = []

    def pop_body(self):
        """Replace the current body with the last one pushed to the stack."""

        self.body = self.body_stack.pop()

    def visit_slideconf(self, node):
        pass

    def depart_slideconf(self, node):
        pass

    def _add_slide_number(self, slide_no):
        """Add the slide number to the output if enabled."""

        if self.builder.config.slide_numbers:
            self.body.append(
                '\n<div class="slide-no">%s</div>\n' % (slide_no,),
            )

    def _add_slide_footer(self, slide_no):
        """Add the slide footer to the output if enabled."""

        if self.builder.config.slide_footer:
            self.body.append(
                '\n<div class="slide-footer">%s</div>\n' % (
                    self.builder.config.slide_footer,
                ),
            )

    def visit_slide(self, node):

        from hieroglyph import builder

        slide_level = node.attributes.get('level', self.section_level)

        if slide_level > self.builder.config.slide_levels:
            # dummy for matching div's
            self.body.append(
                self.starttag(
                    node, 'div', CLASS='section level-%s' % slide_level)
            )
            node.tag_name = 'div'
        else:

            slide_conf = slideconf.get_conf(self.builder, node.document)
            if (builder.building_slides(self.builder.app) and
                    slide_conf['autoslides'] and
                    isinstance(node.parent, nodes.section) and
                    not getattr(node.parent, 'closed', False)):

                # we're building slides and creating slides from
                # sections; close the previous section, if needed
                self.depart_slide(node.parent)

            # don't increment section_count until we've (potentially)
            # closed the previous slide
            self.section_count += 1

            node.closed = False

            classes = node.get('classes')
            if not classes:
                classes = slide_conf['slide_classes']

            # self.body.append(
            #     self.starttag(
            #         node, 'article',
            #         CLASS='%s slide level-%s' % (
            #             ' '.join(classes),
            #             slide_level,
            #         ),
            #     )
            # )
            node.tag_name = 'article'

            slide_id = node.get('ids')
            if slide_id:
                slide_id = slide_id[0]
            else:
                slide_id = ''

            assert self.current_slide is None
            self.current_slide = SlideData(
                self,
                id=slide_id,
                level=slide_level,
                classes=classes,
                slide_number=self.section_count,
            )
            self.push_body()

    def depart_slide(self, node):

        if self.current_slide and not getattr(node, 'closed', False):

            # mark the slide closed
            node.closed = True

            # self._add_slide_footer(self.section_count)
            # self._add_slide_number(self.section_count)
            # self.body.append(
            #     '\n</%s>\n' % getattr(node, 'tag_name', 'article')
            # )

            self.current_slide.content = ''.join(self.body)
            self.pop_body()
            rendered_slide = self.builder.templates.render(
                'slide.html',
                self.current_slide.get_slide_context(),
            )
            self.body.append(rendered_slide)
            self.slide_data.append(self.current_slide)
            self.current_slide = None

    def visit_title(self, node):

        self.push_body()

        if (isinstance(node.parent, slide) or
                node.parent.attributes.get('include-as-slide', False)):
            slide_level = node.parent.attributes.get(
                'level',
                self.section_level)
            level = max(
                slide_level + self.initial_header_level - 1,
                1,
            )
            self.current_slide.level = level

            # tag = 'h%s' % level
            # self.body.append(self.starttag(node, tag, ''))
            # self.context.append('</%s>\n' % tag)

        if self.current_slide and isinstance(node.parent, (nodes.section, slide)):
            self.current_slide.title = node.astext().strip()
        else:
            HTMLTranslator.visit_title(self, node)


    def depart_title(self, node):

        if self.current_slide and isinstance(node.parent, (nodes.section, slide)):
            self.current_slide.title = ''.join(self.body)
            self.pop_body()
        else:
            HTMLTranslator.depart_title(self, node)
            title = ''.join(self.body)
            self.pop_body()
            self.body.append(title)

    def visit_block_quote(self, node):
        quote_slide_tags = ['paragraph', 'attribution']

        # see if this looks like a quote slide
        if (len(node.children) <= 2 and
            [c.tagname for c in node.children] == quote_slide_tags[:len(node.children)]):

            # process this as a quote slide

            # first child must be a paragraph, process it as a <q> element
            p = node.children[0]
            self.body.append(self.starttag(node, 'q'))
            for text_item in p:
                text_item.walkabout(self)
                self.body.append('</q>\n')

            # optional second child must be an attribution, processing as a <div>
            # following the <q>
            if len(node.children) > 1:
                attr = node.children[1]

                self.body.append(self.starttag(attr, 'div', CLASS="author"))
                for text_item in attr:
                    text_item.walkabout(self)
                    self.body.append('</div>\n')

            # skip all normal processing
            raise nodes.SkipNode

        else:
            return HTMLTranslator.visit_block_quote(self, node)


class SlideTranslator(BaseSlideTranslator):

    def visit_section(self, node):

        # XXX: We're actually removing content that's not in slide
        # nodes with autoslides is false, so it's not clear that we
        # even need this guard.
        if (slideconf.get_conf(self.builder, node.document)['autoslides'] or
                node.attributes.get('include-as-slide', False)):

            self.section_level += 1
            return self.visit_slide(node)

    def depart_section(self, node):

        if (slideconf.get_conf(self.builder, node.document)['autoslides'] or
                node.attributes.get('include-as-slide', False)):

            if self.section_level > self.builder.config.slide_levels:
                self.body.append('</div>')
            else:
                self.depart_slide(node)

            self.section_level -= 1

    def depart_title(self, node):

        if node.parent.hasattr('ids') and node.parent['ids']:
            aname = node.parent['ids'][0]

            if self.builder.app.config.slide_link_to_html:
                self.body.append(
                    u'<a class="headerlink" href="%s#%s" ' % (
                        html.html_path(self.builder),
                        aname,
                    ) +
                    u'title="%s">%s</a>' % (
                        _('View HTML'),
                        self.builder.app.config.slide_html_slide_link_symbol,
                    ))

        BaseSlideTranslator.depart_title(self, node)

    def visit_start_of_file(self, node):
        previous = node.parent
        if isinstance(previous, nodes.compound):
            # step up one more level
            previous = previous.parent

        self.depart_slide(previous)
        self.section_level -= 1

        BaseSlideTranslator.visit_start_of_file(self, node)


class SingleFileSlideTranslator(SlideTranslator):

    def visit_compound(self, node):
        if not 'toctree-wrapper' in node['classes']:
            SlideTranslator.visit_compound(self, node)

    def depart_compound(self, node):
        if not 'toctree-wrapper' in node['classes']:
            SlideTranslator.depart_compound(self, node)