directives.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. # -*- coding: utf-8 -*-
  2. # -----------------------------------------------------------------------------
  3. # Bootstrap RST
  4. # Copyright (c) 2014, Nicolas P. Rougier
  5. # Distributed under the (new) BSD License. See LICENSE.txt for more info.
  6. # -----------------------------------------------------------------------------
  7. from docutils import nodes
  8. from docutils.parsers.rst.directives.body import BasePseudoSection
  9. from docutils.parsers.rst import Directive, directives, states, roles
  10. from docutils.parsers.rst.roles import set_classes
  11. from docutils.nodes import fully_normalize_name, whitespace_normalize_name
  12. from docutils.parsers.rst.directives.tables import Table
  13. from docutils.parsers.rst.roles import set_classes
  14. from docutils.transforms import misc
  15. class button(nodes.Inline, nodes.Element): pass
  16. class progress(nodes.Inline, nodes.Element): pass
  17. class alert(nodes.General, nodes.Element): pass
  18. class callout(nodes.General, nodes.Element): pass
  19. class Alert(Directive):
  20. required_arguments, optional_arguments = 0,0
  21. has_content = True
  22. option_spec = {'type': directives.unchanged,
  23. 'dismissable': directives.flag,
  24. 'class': directives.class_option }
  25. def run(self):
  26. # Raise an error if the directive does not have contents.
  27. self.assert_has_content()
  28. text = '\n'.join(self.content)
  29. # Create the node, to be populated by `nested_parse`.
  30. node = alert(text, **self.options)
  31. node['classes'] = ['alert']
  32. node['classes'] += self.options.get('class', [])
  33. if 'type' in self.options:
  34. node['classes'] += ['alert-%s' % node['type']]
  35. node.dismissable = False
  36. if 'dismissable' in self.options:
  37. node['classes'] += ['alert-dismissable']
  38. node.dismissable = True
  39. # Parse the directive contents.
  40. self.state.nested_parse(self.content, self.content_offset, node)
  41. return [node]
  42. class Callout(Directive):
  43. required_arguments, optional_arguments = 0,1
  44. has_content = True
  45. def run(self):
  46. # Raise an error if the directive does not have contents.
  47. self.assert_has_content()
  48. text = '\n'.join(self.content)
  49. # Create the node, to be populated by `nested_parse`.
  50. node = callout(self.block_text, **self.options)
  51. node['classes'] = ['bs-callout']
  52. if len(self.arguments):
  53. type = 'bs-callout-' + self.arguments[0]
  54. else:
  55. type = 'bs-callout-info'
  56. node['classes'] += [type]
  57. # Parse the directive contents.
  58. self.state.nested_parse(self.content, self.content_offset, node)
  59. return [node]
  60. class Container(Directive):
  61. optional_arguments = 1
  62. final_argument_whitespace = True
  63. option_spec = {'name': directives.unchanged}
  64. has_content = True
  65. default_class = None
  66. def run(self):
  67. self.assert_has_content()
  68. text = '\n'.join(self.content)
  69. try:
  70. if self.arguments:
  71. classes = directives.class_option(self.arguments[0])
  72. else:
  73. classes = self.default_class
  74. except ValueError:
  75. raise self.error(
  76. 'Invalid class attribute value for "%s" directive: "%s".'
  77. % (self.name, self.arguments[0]))
  78. node = nodes.container(text)
  79. node['classes'].extend(classes)
  80. self.add_name(node)
  81. self.state.nested_parse(self.content, self.content_offset, node)
  82. return [node]
  83. class Thumbnail(Container):
  84. default_class = ['thumbnail']
  85. class Caption(Container):
  86. default_class = ['caption']
  87. class Jumbotron(Container):
  88. default_class = ['jumbotron']
  89. class PageHeader(Container):
  90. default_class = ['page-header']
  91. class Lead(Directive):
  92. required_arguments, optional_arguments = 0,0
  93. has_content = True
  94. option_spec = {'class': directives.class_option }
  95. def run(self):
  96. self.assert_has_content()
  97. text = '\n'.join(self.content)
  98. node = nodes.container(text, **self.options)
  99. node['classes'] = ['lead']
  100. node['classes'] += self.options.get('class', [])
  101. self.state.nested_parse(self.content, self.content_offset, node)
  102. return [node]
  103. class Paragraph(Directive):
  104. required_arguments, optional_arguments = 0,0
  105. has_content = True
  106. option_spec = {'class': directives.class_option }
  107. def run(self):
  108. # Raise an error if the directive does not have contents.
  109. self.assert_has_content()
  110. text = '\n'.join(self.content)
  111. # Create the node, to be populated by `nested_parse`.
  112. node = nodes.paragraph(text, **self.options)
  113. node['classes'] += self.options.get('class', [])
  114. # Parse the directive contents.
  115. self.state.nested_parse(self.content, self.content_offset, node)
  116. return [node]
  117. class PageRow(Directive):
  118. """
  119. Directive to declare a container that is column-aware.
  120. """
  121. required_arguments, optional_arguments = 0,1
  122. final_argument_whitespace = True
  123. has_content = True
  124. option_spec = {'class': directives.class_option }
  125. def run(self):
  126. self.assert_has_content()
  127. node = nodes.container(self.content)
  128. node['classes'] = ['row']
  129. if self.arguments:
  130. node['classes'] += [self.arguments[0]]
  131. node['classes'] += self.options.get('class', [])
  132. self.add_name(node)
  133. self.state.nested_parse(self.content, self.content_offset, node)
  134. return [node]
  135. class PageColumn(Directive):
  136. """
  137. Directive to declare column with width and offset.
  138. """
  139. required_arguments, optional_arguments = 0,0
  140. final_argument_whitespace = True
  141. has_content = True
  142. option_spec = {'width': directives.positive_int,
  143. 'offset': directives.positive_int,
  144. 'push': directives.positive_int,
  145. 'pull': directives.positive_int,
  146. 'size': lambda x: directives.choice(x, ('xs', 'sm', 'md', 'lg')),
  147. 'class': directives.class_option }
  148. def run(self):
  149. self.assert_has_content()
  150. text = '\n'.join(self.content)
  151. node = nodes.container(text)
  152. width = self.options.get('width', 1)
  153. size = self.options.get('size', 'md')
  154. node['classes'] += ["col-%s-%d" % (size, width)]
  155. offset = self.options.get('offset', 0)
  156. if offset > 0:
  157. node['classes'] += ["col-%s-offset-%d" % (size, offset)]
  158. push = self.options.get('push', 0)
  159. if push > 0:
  160. node['classes'] += ["col-%s-push-%d" % (size, push)]
  161. pull = self.options.get('pull', 0)
  162. if pull > 0:
  163. node['classes'] += ["col-%s-pull-%d" % (size, pull)]
  164. node['classes'] += self.options.get('class', [])
  165. self.add_name(node)
  166. self.state.nested_parse(self.content, self.content_offset, node)
  167. return [node]
  168. class Button(Directive):
  169. """
  170. Directive to declare a button
  171. """
  172. required_arguments, optional_arguments = 0,0
  173. final_argument_whitespace = True
  174. has_content = True
  175. option_spec = {'class' : directives.class_option,
  176. 'target' : directives.unchanged_required }
  177. def run(self):
  178. self.assert_has_content()
  179. node = button()
  180. node['target'] = self.options.get('target', None)
  181. node['classes'] = self.options.get('class', [])
  182. self.state.nested_parse(self.content, self.content_offset, node)
  183. self.add_name(node)
  184. return [node]
  185. class Progress(Directive):
  186. """
  187. Directive to declare a progress bar.
  188. """
  189. required_arguments, optional_arguments = 0,1
  190. final_argument_whitespace = True
  191. has_content = False
  192. option_spec = { 'class' : directives.class_option,
  193. 'label' : directives.unchanged,
  194. 'value' : directives.unchanged_required,
  195. 'min' : directives.unchanged_required,
  196. 'max' : directives.unchanged_required }
  197. def run(self):
  198. node = progress()
  199. node['classes'] = self.options.get('class', '')
  200. node['value_min'] = self.options.get('min_value', '0')
  201. node['value_max'] = self.options.get('max_value', '100')
  202. node['value'] = self.options.get('value', '50')
  203. node['label'] = self.options.get('label', '')
  204. if self.arguments:
  205. node['value'] = self.arguments[0].rstrip(' %')
  206. #if 'label' not in self.options:
  207. # node['label'] = self.arguments[0]
  208. return [node]
  209. class Header(Directive):
  210. """Contents of document header."""
  211. required_arguments, optional_arguments = 0,1
  212. has_content = True
  213. option_spec = {'class': directives.class_option }
  214. def run(self):
  215. self.assert_has_content()
  216. header = self.state_machine.document.get_decoration().get_header()
  217. header['classes'] += self.options.get('class', [])
  218. if self.arguments:
  219. header['classes'] += [self.arguments[0]]
  220. self.state.nested_parse(self.content, self.content_offset, header)
  221. return []
  222. class Footer(Directive):
  223. """Contents of document footer."""
  224. required_arguments, optional_arguments = 0,1
  225. has_content = True
  226. option_spec = {'class': directives.class_option }
  227. def run(self):
  228. self.assert_has_content()
  229. footer = self.state_machine.document.get_decoration().get_footer()
  230. footer['classes'] += self.options.get('class', [])
  231. if self.arguments:
  232. footer['classes'] += [self.arguments[0]]
  233. self.state.nested_parse(self.content, self.content_offset, footer)
  234. return []
  235. # List item class
  236. # -----------------------------------------------------------------------------
  237. class ItemClass(Directive):
  238. """
  239. Set a "list-class" attribute on the directive content or the next element.
  240. When applied to the next element, a "pending" element is inserted, and a
  241. transform does the work later.
  242. """
  243. required_arguments = 1
  244. optional_arguments = 0
  245. final_argument_whitespace = True
  246. has_content = False
  247. def run(self):
  248. try:
  249. class_value = directives.class_option(self.arguments[0])
  250. except ValueError:
  251. raise self.error(
  252. 'Invalid class attribute value for "%s" directive: "%s".'
  253. % (self.name, self.arguments[0]))
  254. parent = self.state.parent
  255. if isinstance(parent,nodes.list_item):
  256. parent['classes'].extend(class_value)
  257. return []
  258. # PATCH: Make a row inherit from the class attribute
  259. # --------------------------------------------------------------
  260. class ListTable(Table):
  261. """
  262. Implement tables whose data is encoded as a uniform two-level bullet list.
  263. For further ideas, see
  264. http://docutils.sf.net/docs/dev/rst/alternatives.html#list-driven-tables
  265. """
  266. option_spec = {'header-rows': directives.nonnegative_int,
  267. 'stub-columns': directives.nonnegative_int,
  268. 'widths': directives.positive_int_list,
  269. 'class': directives.class_option,
  270. 'name': directives.unchanged}
  271. def run(self):
  272. if not self.content:
  273. error = self.state_machine.reporter.error(
  274. 'The "%s" directive is empty; content required.' % self.name,
  275. nodes.literal_block(self.block_text, self.block_text),
  276. line=self.lineno)
  277. return [error]
  278. title, messages = self.make_title()
  279. node = nodes.Element() # anonymous container for parsing
  280. self.state.nested_parse(self.content, self.content_offset, node)
  281. try:
  282. num_cols, col_widths = self.check_list_content(node)
  283. table_data = [[item.children for item in row_list[0]]
  284. for row_list in node[0]]
  285. header_rows = self.options.get('header-rows', 0)
  286. stub_columns = self.options.get('stub-columns', 0)
  287. self.check_table_dimensions(table_data, header_rows, stub_columns)
  288. except SystemMessagePropagation as detail:
  289. return [detail.args[0]]
  290. #table_node = self.build_table_from_list(table_data, col_widths,
  291. # header_rows, stub_columns)
  292. table_node = self.build_table_from_list(node[0], col_widths,
  293. header_rows, stub_columns)
  294. table_node['classes'] += self.options.get('class', [])
  295. self.add_name(table_node)
  296. if title:
  297. table_node.insert(0, title)
  298. return [table_node] + messages
  299. def check_list_content(self, node):
  300. if len(node) != 1 or not isinstance(node[0], nodes.bullet_list):
  301. error = self.state_machine.reporter.error(
  302. 'Error parsing content block for the "%s" directive: '
  303. 'exactly one bullet list expected.' % self.name,
  304. nodes.literal_block(self.block_text, self.block_text),
  305. line=self.lineno)
  306. raise SystemMessagePropagation(error)
  307. list_node = node[0]
  308. # Check for a uniform two-level bullet list:
  309. for item_index in range(len(list_node)):
  310. item = list_node[item_index]
  311. if len(item) != 1 or not isinstance(item[0], nodes.bullet_list):
  312. error = self.state_machine.reporter.error(
  313. 'Error parsing content block for the "%s" directive: '
  314. 'two-level bullet list expected, but row %s does not '
  315. 'contain a second-level bullet list.'
  316. % (self.name, item_index + 1), nodes.literal_block(
  317. self.block_text, self.block_text), line=self.lineno)
  318. raise SystemMessagePropagation(error)
  319. elif item_index:
  320. # ATTN pychecker users: num_cols is guaranteed to be set in the
  321. # "else" clause below for item_index==0, before this branch is
  322. # triggered.
  323. if len(item[0]) != num_cols:
  324. error = self.state_machine.reporter.error(
  325. 'Error parsing content block for the "%s" directive: '
  326. 'uniform two-level bullet list expected, but row %s '
  327. 'does not contain the same number of items as row 1 '
  328. '(%s vs %s).'
  329. % (self.name, item_index + 1, len(item[0]), num_cols),
  330. nodes.literal_block(self.block_text, self.block_text),
  331. line=self.lineno)
  332. raise SystemMessagePropagation(error)
  333. else:
  334. num_cols = len(item[0])
  335. col_widths = self.get_column_widths(num_cols)
  336. return num_cols, col_widths
  337. def build_table_from_list(Self, table_data, col_widths, header_rows, stub_columns):
  338. table = nodes.table()
  339. tgroup = nodes.tgroup(cols=len(col_widths))
  340. table += tgroup
  341. for col_width in col_widths:
  342. colspec = nodes.colspec(colwidth=col_width)
  343. if stub_columns:
  344. colspec.attributes['stub'] = 1
  345. stub_columns -= 1
  346. tgroup += colspec
  347. rows = []
  348. for row in table_data:
  349. row_node = nodes.row()
  350. row_node['classes'] = row[0]['classes']
  351. for cell in row[0]:
  352. cell = cell.children
  353. entry = nodes.entry()
  354. entry += cell
  355. row_node += entry
  356. rows.append(row_node)
  357. if header_rows:
  358. thead = nodes.thead()
  359. thead.extend(rows[:header_rows])
  360. tgroup += thead
  361. tbody = nodes.tbody()
  362. tbody.extend(rows[header_rows:])
  363. tgroup += tbody
  364. return table
  365. directives.register_directive('item-class', ItemClass)
  366. directives.register_directive('list-table', ListTable)
  367. directives.register_directive('thumbnail', Thumbnail)
  368. directives.register_directive('caption', Caption)
  369. directives.register_directive('jumbotron', Jumbotron)
  370. directives.register_directive('page-header', PageHeader)
  371. directives.register_directive('lead', Lead)
  372. directives.register_directive('progress', Progress)
  373. directives.register_directive('alert', Alert)
  374. directives.register_directive('callout', Callout)
  375. directives.register_directive('row', PageRow)
  376. directives.register_directive('column', PageColumn)
  377. directives.register_directive('button', Button)
  378. directives.register_directive('footer', Footer)
  379. directives.register_directive('header', Header)