forked from varia/varia.website
544 lines
15 KiB
Python
544 lines
15 KiB
Python
#!/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)
|