#!/usr/bin/env python # -*- coding: utf-8 -*- """ Twitter Bootstrap RST directives Plugin For Pelican =================================================== This plugin defines rst directives for different CSS and Javascript components from the twitter bootstrap framework. """ from uuid import uuid1 from cgi import escape from docutils import nodes, utils import docutils from docutils.parsers import rst from docutils.parsers.rst import directives, roles, Directive from pelican import signals from pelican.readers import RstReader, PelicanHTMLTranslator class CleanHTMLTranslator(PelicanHTMLTranslator): """ A custom HTML translator based on the Pelican HTML translator. Used to clean up some components html classes that could conflict with the bootstrap CSS classes. Also defines new tags that are not handleed by the current implementation of docutils. The most obvious example is the Container component """ def visit_literal(self, node): classes = node.get('classes', node.get('class', [])) if 'code' in classes: self.body.append(self.starttag(node, 'code')) elif 'kbd' in classes: self.body.append(self.starttag(node, 'kbd')) else: self.body.append(self.starttag(node, 'pre')) def depart_literal(self, node): classes = node.get('classes', node.get('class', [])) if 'code' in classes: self.body.append('</code>\n') elif 'kbd' in classes: self.body.append('</kbd>\n') else: self.body.append('</pre>\n') def visit_container(self, node): self.body.append(self.starttag(node, 'div')) class CleanRSTReader(RstReader): """ A custom RST reader that behaves exactly like its parent class RstReader with the difference that it uses the CleanHTMLTranslator """ def _get_publisher(self, source_path): extra_params = {'initial_header_level': '2', 'syntax_highlight': 'short', 'input_encoding': 'utf-8'} user_params = self.settings.get('DOCUTILS_SETTINGS') if user_params: extra_params.update(user_params) pub = docutils.core.Publisher( destination_class=docutils.io.StringOutput) pub.set_components('standalone', 'restructuredtext', 'html') pub.writer.translator_class = CleanHTMLTranslator pub.process_programmatic_settings(None, extra_params, None) pub.set_source(source_path=source_path) pub.publish() return pub def keyboard_role(name, rawtext, text, lineno, inliner, options={}, content=[]): """ This function creates an inline console input block as defined in the twitter bootstrap documentation overrides the default behaviour of the kbd role *usage:* :kbd:`<your code>` *Example:* :kbd:`<section>` This code is not highlighted """ new_element = nodes.literal(rawtext, text) new_element.set_class('kbd') return [new_element], [] def code_role(name, rawtext, text, lineno, inliner, options={}, content=[]): """ This function creates an inline code block as defined in the twitter bootstrap documentation overrides the default behaviour of the code role *usage:* :code:`<your code>` *Example:* :code:`<section>` This code is not highlighted """ new_element = nodes.literal(rawtext, text) new_element.set_class('code') return [new_element], [] def glyph_role(name, rawtext, text, lineno, inliner, options={}, content=[]): """ This function defines a glyph inline role that show a glyph icon from the twitter bootstrap framework *Usage:* :glyph:`<glyph_name>` *Example:* Love this music :glyph:`music` :) Can be subclassed to include a target *Example:* .. role:: story_time_glyph(glyph) :target: http://www.youtube.com/watch?v=5g8ykQLYnX0 :class: small text-info Love this music :story_time_glyph:`music` :) """ target = options.get('target', None) glyph_name = 'glyphicon-{}'.format(text) if target: target = utils.unescape(target) new_element = nodes.reference(rawtext, ' ', refuri=target) else: new_element = nodes.container() classes = options.setdefault('class', []) classes += ['glyphicon', glyph_name] for custom_class in classes: new_element.set_class(custom_class) return [new_element], [] glyph_role.options = { 'target': rst.directives.unchanged, } glyph_role.content = False class Label(rst.Directive): ''' generic Label directive class definition. This class define a directive that shows bootstrap Labels around its content *usage:* .. label-<label-type>:: <Label content> *example:* .. label-default:: This is a default label content ''' has_content = True custom_class = '' def run(self): # First argument is the name of the glyph label_name = 'label-{}'.format(self.custom_class) # get the label content text = '\n'.join(self.content) # Create a new container element (div) new_element = nodes.container(text) # Update its content self.state.nested_parse(self.content, self.content_offset, new_element) # Set its custom bootstrap classes new_element['classes'] += ['label ', label_name] # Return one single element return [new_element] class DefaultLabel(Label): custom_class = 'default' class PrimaryLabel(Label): custom_class = 'primary' class SuccessLabel(Label): custom_class = 'success' class InfoLabel(Label): custom_class = 'info' class WarningLabel(Label): custom_class = 'warning' class DangerLabel(Label): custom_class = 'danger' class Panel(rst.Directive): """ generic Panel directive class definition. This class define a directive that shows bootstrap Labels around its content *usage:* .. panel-<panel-type>:: :title: <title> <Panel content> *example:* .. panel-default:: :title: panel title This is a default panel content """ has_content = True option_spec = { 'title': rst.directives.unchanged, } custom_class = '' def run(self): # First argument is the name of the glyph panel_name = 'panel-{}'.format(self.custom_class) # get the label title title_text = self.options.get('title', self.custom_class.title()) # get the label content text = '\n'.join(self.content) # Create the panel element panel_element = nodes.container() panel_element['classes'] += ['panel', panel_name] # Create the panel headings heading_element = nodes.container(title_text) title_nodes, messages = self.state.inline_text(title_text, self.lineno) title = nodes.paragraph(title_text, '', *title_nodes) heading_element.append(title) heading_element['classes'] += ['panel-heading'] # Create a new container element (div) body_element = nodes.container(text) # Update its content self.state.nested_parse(self.content, self.content_offset, body_element) # Set its custom bootstrap classes body_element['classes'] += ['panel-body'] # add the heading and body to the panel panel_element.append(heading_element) panel_element.append(body_element) # Return the panel element return [panel_element] class DefaultPanel(Panel): custom_class = 'default' class PrimaryPanel(Panel): custom_class = 'primary' class SuccessPanel(Panel): custom_class = 'success' class InfoPanel(Panel): custom_class = 'info' class WarningPanel(Panel): custom_class = 'warning' class DangerPanel(Panel): custom_class = 'danger' class Alert(rst.Directive): """ generic Alert directive class definition. This class define a directive that shows bootstrap Labels around its content *usage:* .. alert-<alert-type>:: <alert content> *example:* .. alert-warning:: This is a warning alert content """ has_content = True custom_class = '' def run(self): # First argument is the name of the glyph alert_name = 'alert-{}'.format(self.custom_class) # get the label content text = '\n'.join(self.content) # Create a new container element (div) new_element = nodes.compound(text) # Update its content self.state.nested_parse(self.content, self.content_offset, new_element) # Recurse inside its children and change the hyperlinks classes for child in new_element.traverse(include_self=False): if isinstance(child, nodes.reference): child.set_class('alert-link') # Set its custom bootstrap classes new_element['classes'] += ['alert ', alert_name] # Return one single element return [new_element] class SuccessAlert(Alert): custom_class = 'success' class InfoAlert(Alert): custom_class = 'info' class WarningAlert(Alert): custom_class = 'warning' class DangerAlert(Alert): custom_class = 'danger' class Media(rst.Directive): ''' generic Media directive class definition. This class define a directive that shows bootstrap media image with text according to the media component on bootstrap *usage*: .. media:: <image_uri> :position: <position> :alt: <alt> :height: <height> :width: <width> :scale: <scale> :target: <target> <text content> *example*: .. media:: http://stuffkit.com/wp-content/uploads/2012/11/Worlds-Most-Beautiful-Lady-Camilla-Belle-HD-Photos-4.jpg :height: 750 :width: 1000 :scale: 20 :target: www.google.com :alt: Camilla Belle :position: left This image is not mine. Credit goes to http://stuffkit.com ''' has_content = True required_arguments = 1 option_spec = { 'position': str, 'alt': rst.directives.unchanged, 'height': rst.directives.length_or_unitless, 'width': rst.directives.length_or_percentage_or_unitless, 'scale': rst.directives.percentage, 'target': rst.directives.unchanged_required, } def get_image_element(self): # Get the image url image_url = self.arguments[0] image_reference = rst.directives.uri(image_url) self.options['uri'] = image_reference reference_node = None messages = [] if 'target' in self.options: block = rst.states.escape2null( self.options['target']).splitlines() block = [line for line in block] target_type, data = self.state.parse_target( block, self.block_text, self.lineno) if target_type == 'refuri': container_node = nodes.reference(refuri=data) elif target_type == 'refname': container_node = nodes.reference( refname=fully_normalize_name(data), name=whitespace_normalize_name(data)) container_node.indirect_reference_name = data self.state.document.note_refname(container_node) else: # malformed target messages.append(data) # data is a system message del self.options['target'] else: container_node = nodes.container() # get image position position = self.options.get('position', 'left') position_class = 'pull-{}'.format(position) container_node.set_class(position_class) image_node = nodes.image(self.block_text, **self.options) image_node['classes'] += ['media-object'] container_node.append(image_node) return container_node def run(self): # now we get the content text = '\n'.join(self.content) # get image alternative text alternative_text = self.options.get('alternative-text', '') # get container element container_element = nodes.container() container_element['classes'] += ['media'] # get image element image_element = self.get_image_element() # get body element body_element = nodes.container(text) body_element['classes'] += ['media-body'] self.state.nested_parse(self.content, self.content_offset, body_element) container_element.append(image_element) container_element.append(body_element) return [container_element, ] def register_directives(): rst.directives.register_directive('label-default', DefaultLabel) rst.directives.register_directive('label-primary', PrimaryLabel) rst.directives.register_directive('label-success', SuccessLabel) rst.directives.register_directive('label-info', InfoLabel) rst.directives.register_directive('label-warning', WarningLabel) rst.directives.register_directive('label-danger', DangerLabel) rst.directives.register_directive('panel-default', DefaultPanel) rst.directives.register_directive('panel-primary', PrimaryPanel) rst.directives.register_directive('panel-success', SuccessPanel) rst.directives.register_directive('panel-info', InfoPanel) rst.directives.register_directive('panel-warning', WarningPanel) rst.directives.register_directive('panel-danger', DangerPanel) rst.directives.register_directive('alert-success', SuccessAlert) rst.directives.register_directive('alert-info', InfoAlert) rst.directives.register_directive('alert-warning', WarningAlert) rst.directives.register_directive('alert-danger', DangerAlert) rst.directives.register_directive( 'media', Media ) def register_roles(): rst.roles.register_local_role('glyph', glyph_role) rst.roles.register_local_role('code', code_role) rst.roles.register_local_role('kbd', keyboard_role) def add_reader(readers): readers.reader_classes['rst'] = CleanRSTReader def register(): register_directives() register_roles() signals.readers_init.connect(add_reader)