bootstrap_rst_directives.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """
  4. Twitter Bootstrap RST directives Plugin For Pelican
  5. ===================================================
  6. This plugin defines rst directives for different CSS and Javascript components from
  7. the twitter bootstrap framework.
  8. """
  9. from uuid import uuid1
  10. from cgi import escape
  11. from docutils import nodes, utils
  12. import docutils
  13. from docutils.parsers import rst
  14. from docutils.parsers.rst import directives, roles, Directive
  15. from pelican import signals
  16. from pelican.readers import RstReader, PelicanHTMLTranslator
  17. class CleanHTMLTranslator(PelicanHTMLTranslator):
  18. """
  19. A custom HTML translator based on the Pelican HTML translator.
  20. Used to clean up some components html classes that could conflict
  21. with the bootstrap CSS classes.
  22. Also defines new tags that are not handleed by the current implementation of
  23. docutils.
  24. The most obvious example is the Container component
  25. """
  26. def visit_literal(self, node):
  27. classes = node.get('classes', node.get('class', []))
  28. if 'code' in classes:
  29. self.body.append(self.starttag(node, 'code'))
  30. elif 'kbd' in classes:
  31. self.body.append(self.starttag(node, 'kbd'))
  32. else:
  33. self.body.append(self.starttag(node, 'pre'))
  34. def depart_literal(self, node):
  35. classes = node.get('classes', node.get('class', []))
  36. if 'code' in classes:
  37. self.body.append('</code>\n')
  38. elif 'kbd' in classes:
  39. self.body.append('</kbd>\n')
  40. else:
  41. self.body.append('</pre>\n')
  42. def visit_container(self, node):
  43. self.body.append(self.starttag(node, 'div'))
  44. class CleanRSTReader(RstReader):
  45. """
  46. A custom RST reader that behaves exactly like its parent class RstReader with
  47. the difference that it uses the CleanHTMLTranslator
  48. """
  49. def _get_publisher(self, source_path):
  50. extra_params = {'initial_header_level': '2',
  51. 'syntax_highlight': 'short',
  52. 'input_encoding': 'utf-8'}
  53. user_params = self.settings.get('DOCUTILS_SETTINGS')
  54. if user_params:
  55. extra_params.update(user_params)
  56. pub = docutils.core.Publisher(
  57. destination_class=docutils.io.StringOutput)
  58. pub.set_components('standalone', 'restructuredtext', 'html')
  59. pub.writer.translator_class = CleanHTMLTranslator
  60. pub.process_programmatic_settings(None, extra_params, None)
  61. pub.set_source(source_path=source_path)
  62. pub.publish()
  63. return pub
  64. def keyboard_role(name, rawtext, text, lineno, inliner,
  65. options={}, content=[]):
  66. """
  67. This function creates an inline console input block as defined in the twitter bootstrap documentation
  68. overrides the default behaviour of the kbd role
  69. *usage:*
  70. :kbd:`<your code>`
  71. *Example:*
  72. :kbd:`<section>`
  73. This code is not highlighted
  74. """
  75. new_element = nodes.literal(rawtext, text)
  76. new_element.set_class('kbd')
  77. return [new_element], []
  78. def code_role(name, rawtext, text, lineno, inliner,
  79. options={}, content=[]):
  80. """
  81. This function creates an inline code block as defined in the twitter bootstrap documentation
  82. overrides the default behaviour of the code role
  83. *usage:*
  84. :code:`<your code>`
  85. *Example:*
  86. :code:`<section>`
  87. This code is not highlighted
  88. """
  89. new_element = nodes.literal(rawtext, text)
  90. new_element.set_class('code')
  91. return [new_element], []
  92. def glyph_role(name, rawtext, text, lineno, inliner,
  93. options={}, content=[]):
  94. """
  95. This function defines a glyph inline role that show a glyph icon from the
  96. twitter bootstrap framework
  97. *Usage:*
  98. :glyph:`<glyph_name>`
  99. *Example:*
  100. Love this music :glyph:`music` :)
  101. Can be subclassed to include a target
  102. *Example:*
  103. .. role:: story_time_glyph(glyph)
  104. :target: http://www.youtube.com/watch?v=5g8ykQLYnX0
  105. :class: small text-info
  106. Love this music :story_time_glyph:`music` :)
  107. """
  108. target = options.get('target', None)
  109. glyph_name = 'glyphicon-{}'.format(text)
  110. if target:
  111. target = utils.unescape(target)
  112. new_element = nodes.reference(rawtext, ' ', refuri=target)
  113. else:
  114. new_element = nodes.container()
  115. classes = options.setdefault('class', [])
  116. classes += ['glyphicon', glyph_name]
  117. for custom_class in classes:
  118. new_element.set_class(custom_class)
  119. return [new_element], []
  120. glyph_role.options = {
  121. 'target': rst.directives.unchanged,
  122. }
  123. glyph_role.content = False
  124. class Label(rst.Directive):
  125. '''
  126. generic Label directive class definition.
  127. This class define a directive that shows
  128. bootstrap Labels around its content
  129. *usage:*
  130. .. label-<label-type>::
  131. <Label content>
  132. *example:*
  133. .. label-default::
  134. This is a default label content
  135. '''
  136. has_content = True
  137. custom_class = ''
  138. def run(self):
  139. # First argument is the name of the glyph
  140. label_name = 'label-{}'.format(self.custom_class)
  141. # get the label content
  142. text = '\n'.join(self.content)
  143. # Create a new container element (div)
  144. new_element = nodes.container(text)
  145. # Update its content
  146. self.state.nested_parse(self.content, self.content_offset,
  147. new_element)
  148. # Set its custom bootstrap classes
  149. new_element['classes'] += ['label ', label_name]
  150. # Return one single element
  151. return [new_element]
  152. class DefaultLabel(Label):
  153. custom_class = 'default'
  154. class PrimaryLabel(Label):
  155. custom_class = 'primary'
  156. class SuccessLabel(Label):
  157. custom_class = 'success'
  158. class InfoLabel(Label):
  159. custom_class = 'info'
  160. class WarningLabel(Label):
  161. custom_class = 'warning'
  162. class DangerLabel(Label):
  163. custom_class = 'danger'
  164. class Panel(rst.Directive):
  165. """
  166. generic Panel directive class definition.
  167. This class define a directive that shows
  168. bootstrap Labels around its content
  169. *usage:*
  170. .. panel-<panel-type>::
  171. :title: <title>
  172. <Panel content>
  173. *example:*
  174. .. panel-default::
  175. :title: panel title
  176. This is a default panel content
  177. """
  178. has_content = True
  179. option_spec = {
  180. 'title': rst.directives.unchanged,
  181. }
  182. custom_class = ''
  183. def run(self):
  184. # First argument is the name of the glyph
  185. panel_name = 'panel-{}'.format(self.custom_class)
  186. # get the label title
  187. title_text = self.options.get('title', self.custom_class.title())
  188. # get the label content
  189. text = '\n'.join(self.content)
  190. # Create the panel element
  191. panel_element = nodes.container()
  192. panel_element['classes'] += ['panel', panel_name]
  193. # Create the panel headings
  194. heading_element = nodes.container(title_text)
  195. title_nodes, messages = self.state.inline_text(title_text,
  196. self.lineno)
  197. title = nodes.paragraph(title_text, '', *title_nodes)
  198. heading_element.append(title)
  199. heading_element['classes'] += ['panel-heading']
  200. # Create a new container element (div)
  201. body_element = nodes.container(text)
  202. # Update its content
  203. self.state.nested_parse(self.content, self.content_offset,
  204. body_element)
  205. # Set its custom bootstrap classes
  206. body_element['classes'] += ['panel-body']
  207. # add the heading and body to the panel
  208. panel_element.append(heading_element)
  209. panel_element.append(body_element)
  210. # Return the panel element
  211. return [panel_element]
  212. class DefaultPanel(Panel):
  213. custom_class = 'default'
  214. class PrimaryPanel(Panel):
  215. custom_class = 'primary'
  216. class SuccessPanel(Panel):
  217. custom_class = 'success'
  218. class InfoPanel(Panel):
  219. custom_class = 'info'
  220. class WarningPanel(Panel):
  221. custom_class = 'warning'
  222. class DangerPanel(Panel):
  223. custom_class = 'danger'
  224. class Alert(rst.Directive):
  225. """
  226. generic Alert directive class definition.
  227. This class define a directive that shows
  228. bootstrap Labels around its content
  229. *usage:*
  230. .. alert-<alert-type>::
  231. <alert content>
  232. *example:*
  233. .. alert-warning::
  234. This is a warning alert content
  235. """
  236. has_content = True
  237. custom_class = ''
  238. def run(self):
  239. # First argument is the name of the glyph
  240. alert_name = 'alert-{}'.format(self.custom_class)
  241. # get the label content
  242. text = '\n'.join(self.content)
  243. # Create a new container element (div)
  244. new_element = nodes.compound(text)
  245. # Update its content
  246. self.state.nested_parse(self.content, self.content_offset,
  247. new_element)
  248. # Recurse inside its children and change the hyperlinks classes
  249. for child in new_element.traverse(include_self=False):
  250. if isinstance(child, nodes.reference):
  251. child.set_class('alert-link')
  252. # Set its custom bootstrap classes
  253. new_element['classes'] += ['alert ', alert_name]
  254. # Return one single element
  255. return [new_element]
  256. class SuccessAlert(Alert):
  257. custom_class = 'success'
  258. class InfoAlert(Alert):
  259. custom_class = 'info'
  260. class WarningAlert(Alert):
  261. custom_class = 'warning'
  262. class DangerAlert(Alert):
  263. custom_class = 'danger'
  264. class Media(rst.Directive):
  265. '''
  266. generic Media directive class definition.
  267. This class define a directive that shows
  268. bootstrap media image with text according
  269. to the media component on bootstrap
  270. *usage*:
  271. .. media:: <image_uri>
  272. :position: <position>
  273. :alt: <alt>
  274. :height: <height>
  275. :width: <width>
  276. :scale: <scale>
  277. :target: <target>
  278. <text content>
  279. *example*:
  280. .. media:: http://stuffkit.com/wp-content/uploads/2012/11/Worlds-Most-Beautiful-Lady-Camilla-Belle-HD-Photos-4.jpg
  281. :height: 750
  282. :width: 1000
  283. :scale: 20
  284. :target: www.google.com
  285. :alt: Camilla Belle
  286. :position: left
  287. This image is not mine. Credit goes to http://stuffkit.com
  288. '''
  289. has_content = True
  290. required_arguments = 1
  291. option_spec = {
  292. 'position': str,
  293. 'alt': rst.directives.unchanged,
  294. 'height': rst.directives.length_or_unitless,
  295. 'width': rst.directives.length_or_percentage_or_unitless,
  296. 'scale': rst.directives.percentage,
  297. 'target': rst.directives.unchanged_required,
  298. }
  299. def get_image_element(self):
  300. # Get the image url
  301. image_url = self.arguments[0]
  302. image_reference = rst.directives.uri(image_url)
  303. self.options['uri'] = image_reference
  304. reference_node = None
  305. messages = []
  306. if 'target' in self.options:
  307. block = rst.states.escape2null(
  308. self.options['target']).splitlines()
  309. block = [line for line in block]
  310. target_type, data = self.state.parse_target(
  311. block, self.block_text, self.lineno)
  312. if target_type == 'refuri':
  313. container_node = nodes.reference(refuri=data)
  314. elif target_type == 'refname':
  315. container_node = nodes.reference(
  316. refname=fully_normalize_name(data),
  317. name=whitespace_normalize_name(data))
  318. container_node.indirect_reference_name = data
  319. self.state.document.note_refname(container_node)
  320. else: # malformed target
  321. messages.append(data) # data is a system message
  322. del self.options['target']
  323. else:
  324. container_node = nodes.container()
  325. # get image position
  326. position = self.options.get('position', 'left')
  327. position_class = 'pull-{}'.format(position)
  328. container_node.set_class(position_class)
  329. image_node = nodes.image(self.block_text, **self.options)
  330. image_node['classes'] += ['media-object']
  331. container_node.append(image_node)
  332. return container_node
  333. def run(self):
  334. # now we get the content
  335. text = '\n'.join(self.content)
  336. # get image alternative text
  337. alternative_text = self.options.get('alternative-text', '')
  338. # get container element
  339. container_element = nodes.container()
  340. container_element['classes'] += ['media']
  341. # get image element
  342. image_element = self.get_image_element()
  343. # get body element
  344. body_element = nodes.container(text)
  345. body_element['classes'] += ['media-body']
  346. self.state.nested_parse(self.content, self.content_offset,
  347. body_element)
  348. container_element.append(image_element)
  349. container_element.append(body_element)
  350. return [container_element, ]
  351. def register_directives():
  352. rst.directives.register_directive('label-default', DefaultLabel)
  353. rst.directives.register_directive('label-primary', PrimaryLabel)
  354. rst.directives.register_directive('label-success', SuccessLabel)
  355. rst.directives.register_directive('label-info', InfoLabel)
  356. rst.directives.register_directive('label-warning', WarningLabel)
  357. rst.directives.register_directive('label-danger', DangerLabel)
  358. rst.directives.register_directive('panel-default', DefaultPanel)
  359. rst.directives.register_directive('panel-primary', PrimaryPanel)
  360. rst.directives.register_directive('panel-success', SuccessPanel)
  361. rst.directives.register_directive('panel-info', InfoPanel)
  362. rst.directives.register_directive('panel-warning', WarningPanel)
  363. rst.directives.register_directive('panel-danger', DangerPanel)
  364. rst.directives.register_directive('alert-success', SuccessAlert)
  365. rst.directives.register_directive('alert-info', InfoAlert)
  366. rst.directives.register_directive('alert-warning', WarningAlert)
  367. rst.directives.register_directive('alert-danger', DangerAlert)
  368. rst.directives.register_directive( 'media', Media )
  369. def register_roles():
  370. rst.roles.register_local_role('glyph', glyph_role)
  371. rst.roles.register_local_role('code', code_role)
  372. rst.roles.register_local_role('kbd', keyboard_role)
  373. def add_reader(readers):
  374. readers.reader_classes['rst'] = CleanRSTReader
  375. def register():
  376. register_directives()
  377. register_roles()
  378. signals.readers_init.connect(add_reader)