|
@@ -0,0 +1,465 @@
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
+# -----------------------------------------------------------------------------
|
|
|
+# Bootstrap RST
|
|
|
+# Copyright (c) 2014, Nicolas P. Rougier
|
|
|
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
|
|
|
+# -----------------------------------------------------------------------------
|
|
|
+from docutils import nodes
|
|
|
+from docutils.parsers.rst.directives.body import BasePseudoSection
|
|
|
+from docutils.parsers.rst import Directive, directives, states, roles
|
|
|
+from docutils.parsers.rst.roles import set_classes
|
|
|
+from docutils.nodes import fully_normalize_name, whitespace_normalize_name
|
|
|
+from docutils.parsers.rst.directives.tables import Table
|
|
|
+from docutils.parsers.rst.roles import set_classes
|
|
|
+from docutils.transforms import misc
|
|
|
+
|
|
|
+
|
|
|
+class button(nodes.Inline, nodes.Element): pass
|
|
|
+class progress(nodes.Inline, nodes.Element): pass
|
|
|
+class alert(nodes.General, nodes.Element): pass
|
|
|
+class callout(nodes.General, nodes.Element): pass
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+class Alert(Directive):
|
|
|
+ required_arguments, optional_arguments = 0,0
|
|
|
+ has_content = True
|
|
|
+ option_spec = {'type': directives.unchanged,
|
|
|
+ 'dismissable': directives.flag,
|
|
|
+ 'class': directives.class_option }
|
|
|
+
|
|
|
+ def run(self):
|
|
|
+ # Raise an error if the directive does not have contents.
|
|
|
+ self.assert_has_content()
|
|
|
+ text = '\n'.join(self.content)
|
|
|
+
|
|
|
+ # Create the node, to be populated by `nested_parse`.
|
|
|
+ node = alert(text, **self.options)
|
|
|
+ node['classes'] = ['alert']
|
|
|
+ node['classes'] += self.options.get('class', [])
|
|
|
+ if 'type' in self.options:
|
|
|
+ node['classes'] += ['alert-%s' % node['type']]
|
|
|
+ node.dismissable = False
|
|
|
+ if 'dismissable' in self.options:
|
|
|
+ node['classes'] += ['alert-dismissable']
|
|
|
+ node.dismissable = True
|
|
|
+
|
|
|
+ # Parse the directive contents.
|
|
|
+ self.state.nested_parse(self.content, self.content_offset, node)
|
|
|
+ return [node]
|
|
|
+
|
|
|
+
|
|
|
+class Callout(Directive):
|
|
|
+ required_arguments, optional_arguments = 0,1
|
|
|
+ has_content = True
|
|
|
+
|
|
|
+ def run(self):
|
|
|
+ # Raise an error if the directive does not have contents.
|
|
|
+ self.assert_has_content()
|
|
|
+ text = '\n'.join(self.content)
|
|
|
+
|
|
|
+ # Create the node, to be populated by `nested_parse`.
|
|
|
+ node = callout(self.block_text, **self.options)
|
|
|
+ node['classes'] = ['bs-callout']
|
|
|
+ if len(self.arguments):
|
|
|
+ type = 'bs-callout-' + self.arguments[0]
|
|
|
+ else:
|
|
|
+ type = 'bs-callout-info'
|
|
|
+ node['classes'] += [type]
|
|
|
+
|
|
|
+ # Parse the directive contents.
|
|
|
+ self.state.nested_parse(self.content, self.content_offset, node)
|
|
|
+ return [node]
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+class Container(Directive):
|
|
|
+ optional_arguments = 1
|
|
|
+ final_argument_whitespace = True
|
|
|
+ option_spec = {'name': directives.unchanged}
|
|
|
+ has_content = True
|
|
|
+ default_class = None
|
|
|
+
|
|
|
+ def run(self):
|
|
|
+ self.assert_has_content()
|
|
|
+ text = '\n'.join(self.content)
|
|
|
+ try:
|
|
|
+ if self.arguments:
|
|
|
+ classes = directives.class_option(self.arguments[0])
|
|
|
+ else:
|
|
|
+ classes = self.default_class
|
|
|
+ except ValueError:
|
|
|
+ raise self.error(
|
|
|
+ 'Invalid class attribute value for "%s" directive: "%s".'
|
|
|
+ % (self.name, self.arguments[0]))
|
|
|
+ node = nodes.container(text)
|
|
|
+ node['classes'].extend(classes)
|
|
|
+ self.add_name(node)
|
|
|
+ self.state.nested_parse(self.content, self.content_offset, node)
|
|
|
+ return [node]
|
|
|
+
|
|
|
+class Thumbnail(Container):
|
|
|
+ default_class = ['thumbnail']
|
|
|
+
|
|
|
+class Caption(Container):
|
|
|
+ default_class = ['caption']
|
|
|
+
|
|
|
+class Jumbotron(Container):
|
|
|
+ default_class = ['jumbotron']
|
|
|
+
|
|
|
+class PageHeader(Container):
|
|
|
+ default_class = ['page-header']
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+class Lead(Directive):
|
|
|
+ required_arguments, optional_arguments = 0,0
|
|
|
+ has_content = True
|
|
|
+ option_spec = {'class': directives.class_option }
|
|
|
+ def run(self):
|
|
|
+ self.assert_has_content()
|
|
|
+ text = '\n'.join(self.content)
|
|
|
+ node = nodes.container(text, **self.options)
|
|
|
+ node['classes'] = ['lead']
|
|
|
+ node['classes'] += self.options.get('class', [])
|
|
|
+ self.state.nested_parse(self.content, self.content_offset, node)
|
|
|
+ return [node]
|
|
|
+
|
|
|
+
|
|
|
+class Paragraph(Directive):
|
|
|
+ required_arguments, optional_arguments = 0,0
|
|
|
+ has_content = True
|
|
|
+ option_spec = {'class': directives.class_option }
|
|
|
+
|
|
|
+ def run(self):
|
|
|
+ # Raise an error if the directive does not have contents.
|
|
|
+ self.assert_has_content()
|
|
|
+ text = '\n'.join(self.content)
|
|
|
+
|
|
|
+ # Create the node, to be populated by `nested_parse`.
|
|
|
+ node = nodes.paragraph(text, **self.options)
|
|
|
+ node['classes'] += self.options.get('class', [])
|
|
|
+
|
|
|
+ # Parse the directive contents.
|
|
|
+ self.state.nested_parse(self.content, self.content_offset, node)
|
|
|
+ return [node]
|
|
|
+
|
|
|
+
|
|
|
+class PageRow(Directive):
|
|
|
+
|
|
|
+ """
|
|
|
+ Directive to declare a container that is column-aware.
|
|
|
+ """
|
|
|
+
|
|
|
+ required_arguments, optional_arguments = 0,1
|
|
|
+ final_argument_whitespace = True
|
|
|
+ has_content = True
|
|
|
+ option_spec = {'class': directives.class_option }
|
|
|
+ def run(self):
|
|
|
+ self.assert_has_content()
|
|
|
+ node = nodes.container(self.content)
|
|
|
+ node['classes'] = ['row']
|
|
|
+ if self.arguments:
|
|
|
+ node['classes'] += [self.arguments[0]]
|
|
|
+ node['classes'] += self.options.get('class', [])
|
|
|
+
|
|
|
+ self.add_name(node)
|
|
|
+ self.state.nested_parse(self.content, self.content_offset, node)
|
|
|
+ return [node]
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+class PageColumn(Directive):
|
|
|
+
|
|
|
+ """
|
|
|
+ Directive to declare column with width and offset.
|
|
|
+ """
|
|
|
+
|
|
|
+ required_arguments, optional_arguments = 0,0
|
|
|
+ final_argument_whitespace = True
|
|
|
+ has_content = True
|
|
|
+ option_spec = {'width': directives.positive_int,
|
|
|
+ 'offset': directives.positive_int,
|
|
|
+ 'push': directives.positive_int,
|
|
|
+ 'pull': directives.positive_int,
|
|
|
+ 'size': lambda x: directives.choice(x, ('xs', 'sm', 'md', 'lg')),
|
|
|
+ 'class': directives.class_option }
|
|
|
+ def run(self):
|
|
|
+ self.assert_has_content()
|
|
|
+ text = '\n'.join(self.content)
|
|
|
+ node = nodes.container(text)
|
|
|
+ width = self.options.get('width', 1)
|
|
|
+ size = self.options.get('size', 'md')
|
|
|
+ node['classes'] += ["col-%s-%d" % (size, width)]
|
|
|
+
|
|
|
+ offset = self.options.get('offset', 0)
|
|
|
+ if offset > 0:
|
|
|
+ node['classes'] += ["col-%s-offset-%d" % (size, offset)]
|
|
|
+
|
|
|
+ push = self.options.get('push', 0)
|
|
|
+ if push > 0:
|
|
|
+ node['classes'] += ["col-%s-push-%d" % (size, push)]
|
|
|
+
|
|
|
+ pull = self.options.get('pull', 0)
|
|
|
+ if pull > 0:
|
|
|
+ node['classes'] += ["col-%s-pull-%d" % (size, pull)]
|
|
|
+
|
|
|
+ node['classes'] += self.options.get('class', [])
|
|
|
+
|
|
|
+ self.add_name(node)
|
|
|
+ self.state.nested_parse(self.content, self.content_offset, node)
|
|
|
+ return [node]
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+class Button(Directive):
|
|
|
+
|
|
|
+ """
|
|
|
+ Directive to declare a button
|
|
|
+ """
|
|
|
+
|
|
|
+ required_arguments, optional_arguments = 0,0
|
|
|
+ final_argument_whitespace = True
|
|
|
+ has_content = True
|
|
|
+ option_spec = {'class' : directives.class_option,
|
|
|
+ 'target' : directives.unchanged_required }
|
|
|
+ def run(self):
|
|
|
+ self.assert_has_content()
|
|
|
+ node = button()
|
|
|
+ node['target'] = self.options.get('target', None)
|
|
|
+ node['classes'] = self.options.get('class', [])
|
|
|
+ self.state.nested_parse(self.content, self.content_offset, node)
|
|
|
+ self.add_name(node)
|
|
|
+ return [node]
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+class Progress(Directive):
|
|
|
+
|
|
|
+ """
|
|
|
+ Directive to declare a progress bar.
|
|
|
+ """
|
|
|
+
|
|
|
+ required_arguments, optional_arguments = 0,1
|
|
|
+ final_argument_whitespace = True
|
|
|
+ has_content = False
|
|
|
+ option_spec = { 'class' : directives.class_option,
|
|
|
+ 'label' : directives.unchanged,
|
|
|
+ 'value' : directives.unchanged_required,
|
|
|
+ 'min' : directives.unchanged_required,
|
|
|
+ 'max' : directives.unchanged_required }
|
|
|
+ def run(self):
|
|
|
+ node = progress()
|
|
|
+ node['classes'] = self.options.get('class', '')
|
|
|
+ node['value_min'] = self.options.get('min_value', '0')
|
|
|
+ node['value_max'] = self.options.get('max_value', '100')
|
|
|
+ node['value'] = self.options.get('value', '50')
|
|
|
+ node['label'] = self.options.get('label', '')
|
|
|
+ if self.arguments:
|
|
|
+ node['value'] = self.arguments[0].rstrip(' %')
|
|
|
+ #if 'label' not in self.options:
|
|
|
+ # node['label'] = self.arguments[0]
|
|
|
+ return [node]
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+class Header(Directive):
|
|
|
+
|
|
|
+ """Contents of document header."""
|
|
|
+
|
|
|
+ required_arguments, optional_arguments = 0,1
|
|
|
+ has_content = True
|
|
|
+ option_spec = {'class': directives.class_option }
|
|
|
+
|
|
|
+ def run(self):
|
|
|
+ self.assert_has_content()
|
|
|
+ header = self.state_machine.document.get_decoration().get_header()
|
|
|
+ header['classes'] += self.options.get('class', [])
|
|
|
+ if self.arguments:
|
|
|
+ header['classes'] += [self.arguments[0]]
|
|
|
+ self.state.nested_parse(self.content, self.content_offset, header)
|
|
|
+ return []
|
|
|
+
|
|
|
+
|
|
|
+class Footer(Directive):
|
|
|
+
|
|
|
+ """Contents of document footer."""
|
|
|
+
|
|
|
+ required_arguments, optional_arguments = 0,1
|
|
|
+ has_content = True
|
|
|
+ option_spec = {'class': directives.class_option }
|
|
|
+
|
|
|
+ def run(self):
|
|
|
+ self.assert_has_content()
|
|
|
+ footer = self.state_machine.document.get_decoration().get_footer()
|
|
|
+ footer['classes'] += self.options.get('class', [])
|
|
|
+ if self.arguments:
|
|
|
+ footer['classes'] += [self.arguments[0]]
|
|
|
+ self.state.nested_parse(self.content, self.content_offset, footer)
|
|
|
+ return []
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+# List item class
|
|
|
+# -----------------------------------------------------------------------------
|
|
|
+class ItemClass(Directive):
|
|
|
+
|
|
|
+ """
|
|
|
+ Set a "list-class" attribute on the directive content or the next element.
|
|
|
+ When applied to the next element, a "pending" element is inserted, and a
|
|
|
+ transform does the work later.
|
|
|
+ """
|
|
|
+
|
|
|
+ required_arguments = 1
|
|
|
+ optional_arguments = 0
|
|
|
+ final_argument_whitespace = True
|
|
|
+ has_content = False
|
|
|
+
|
|
|
+ def run(self):
|
|
|
+ try:
|
|
|
+ class_value = directives.class_option(self.arguments[0])
|
|
|
+ except ValueError:
|
|
|
+ raise self.error(
|
|
|
+ 'Invalid class attribute value for "%s" directive: "%s".'
|
|
|
+ % (self.name, self.arguments[0]))
|
|
|
+
|
|
|
+ parent = self.state.parent
|
|
|
+ if isinstance(parent,nodes.list_item):
|
|
|
+ parent['classes'].extend(class_value)
|
|
|
+ return []
|
|
|
+
|
|
|
+
|
|
|
+# PATCH: Make a row inherit from the class attribute
|
|
|
+# --------------------------------------------------------------
|
|
|
+class ListTable(Table):
|
|
|
+
|
|
|
+ """
|
|
|
+ Implement tables whose data is encoded as a uniform two-level bullet list.
|
|
|
+ For further ideas, see
|
|
|
+ http://docutils.sf.net/docs/dev/rst/alternatives.html#list-driven-tables
|
|
|
+ """
|
|
|
+
|
|
|
+ option_spec = {'header-rows': directives.nonnegative_int,
|
|
|
+ 'stub-columns': directives.nonnegative_int,
|
|
|
+ 'widths': directives.positive_int_list,
|
|
|
+ 'class': directives.class_option,
|
|
|
+ 'name': directives.unchanged}
|
|
|
+
|
|
|
+ def run(self):
|
|
|
+ if not self.content:
|
|
|
+ error = self.state_machine.reporter.error(
|
|
|
+ 'The "%s" directive is empty; content required.' % self.name,
|
|
|
+ nodes.literal_block(self.block_text, self.block_text),
|
|
|
+ line=self.lineno)
|
|
|
+ return [error]
|
|
|
+ title, messages = self.make_title()
|
|
|
+ node = nodes.Element() # anonymous container for parsing
|
|
|
+ self.state.nested_parse(self.content, self.content_offset, node)
|
|
|
+ try:
|
|
|
+ num_cols, col_widths = self.check_list_content(node)
|
|
|
+ table_data = [[item.children for item in row_list[0]]
|
|
|
+ for row_list in node[0]]
|
|
|
+ header_rows = self.options.get('header-rows', 0)
|
|
|
+ stub_columns = self.options.get('stub-columns', 0)
|
|
|
+ self.check_table_dimensions(table_data, header_rows, stub_columns)
|
|
|
+ except SystemMessagePropagation as detail:
|
|
|
+ return [detail.args[0]]
|
|
|
+ #table_node = self.build_table_from_list(table_data, col_widths,
|
|
|
+ # header_rows, stub_columns)
|
|
|
+ table_node = self.build_table_from_list(node[0], col_widths,
|
|
|
+ header_rows, stub_columns)
|
|
|
+ table_node['classes'] += self.options.get('class', [])
|
|
|
+ self.add_name(table_node)
|
|
|
+ if title:
|
|
|
+ table_node.insert(0, title)
|
|
|
+ return [table_node] + messages
|
|
|
+
|
|
|
+ def check_list_content(self, node):
|
|
|
+ if len(node) != 1 or not isinstance(node[0], nodes.bullet_list):
|
|
|
+ error = self.state_machine.reporter.error(
|
|
|
+ 'Error parsing content block for the "%s" directive: '
|
|
|
+ 'exactly one bullet list expected.' % self.name,
|
|
|
+ nodes.literal_block(self.block_text, self.block_text),
|
|
|
+ line=self.lineno)
|
|
|
+ raise SystemMessagePropagation(error)
|
|
|
+ list_node = node[0]
|
|
|
+ # Check for a uniform two-level bullet list:
|
|
|
+ for item_index in range(len(list_node)):
|
|
|
+ item = list_node[item_index]
|
|
|
+ if len(item) != 1 or not isinstance(item[0], nodes.bullet_list):
|
|
|
+ error = self.state_machine.reporter.error(
|
|
|
+ 'Error parsing content block for the "%s" directive: '
|
|
|
+ 'two-level bullet list expected, but row %s does not '
|
|
|
+ 'contain a second-level bullet list.'
|
|
|
+ % (self.name, item_index + 1), nodes.literal_block(
|
|
|
+ self.block_text, self.block_text), line=self.lineno)
|
|
|
+ raise SystemMessagePropagation(error)
|
|
|
+ elif item_index:
|
|
|
+ # ATTN pychecker users: num_cols is guaranteed to be set in the
|
|
|
+ # "else" clause below for item_index==0, before this branch is
|
|
|
+ # triggered.
|
|
|
+ if len(item[0]) != num_cols:
|
|
|
+ error = self.state_machine.reporter.error(
|
|
|
+ 'Error parsing content block for the "%s" directive: '
|
|
|
+ 'uniform two-level bullet list expected, but row %s '
|
|
|
+ 'does not contain the same number of items as row 1 '
|
|
|
+ '(%s vs %s).'
|
|
|
+ % (self.name, item_index + 1, len(item[0]), num_cols),
|
|
|
+ nodes.literal_block(self.block_text, self.block_text),
|
|
|
+ line=self.lineno)
|
|
|
+ raise SystemMessagePropagation(error)
|
|
|
+ else:
|
|
|
+ num_cols = len(item[0])
|
|
|
+ col_widths = self.get_column_widths(num_cols)
|
|
|
+ return num_cols, col_widths
|
|
|
+
|
|
|
+ def build_table_from_list(Self, table_data, col_widths, header_rows, stub_columns):
|
|
|
+ table = nodes.table()
|
|
|
+ tgroup = nodes.tgroup(cols=len(col_widths))
|
|
|
+ table += tgroup
|
|
|
+ for col_width in col_widths:
|
|
|
+ colspec = nodes.colspec(colwidth=col_width)
|
|
|
+ if stub_columns:
|
|
|
+ colspec.attributes['stub'] = 1
|
|
|
+ stub_columns -= 1
|
|
|
+ tgroup += colspec
|
|
|
+ rows = []
|
|
|
+ for row in table_data:
|
|
|
+ row_node = nodes.row()
|
|
|
+ row_node['classes'] = row[0]['classes']
|
|
|
+ for cell in row[0]:
|
|
|
+ cell = cell.children
|
|
|
+ entry = nodes.entry()
|
|
|
+ entry += cell
|
|
|
+ row_node += entry
|
|
|
+ rows.append(row_node)
|
|
|
+ if header_rows:
|
|
|
+ thead = nodes.thead()
|
|
|
+ thead.extend(rows[:header_rows])
|
|
|
+ tgroup += thead
|
|
|
+ tbody = nodes.tbody()
|
|
|
+ tbody.extend(rows[header_rows:])
|
|
|
+ tgroup += tbody
|
|
|
+ return table
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+directives.register_directive('item-class', ItemClass)
|
|
|
+directives.register_directive('list-table', ListTable)
|
|
|
+directives.register_directive('thumbnail', Thumbnail)
|
|
|
+directives.register_directive('caption', Caption)
|
|
|
+directives.register_directive('jumbotron', Jumbotron)
|
|
|
+directives.register_directive('page-header', PageHeader)
|
|
|
+directives.register_directive('lead', Lead)
|
|
|
+directives.register_directive('progress', Progress)
|
|
|
+directives.register_directive('alert', Alert)
|
|
|
+directives.register_directive('callout', Callout)
|
|
|
+directives.register_directive('row', PageRow)
|
|
|
+directives.register_directive('column', PageColumn)
|
|
|
+directives.register_directive('button', Button)
|
|
|
+directives.register_directive('footer', Footer)
|
|
|
+directives.register_directive('header', Header)
|