11""" Tabbed views for Sphinx, with HTML builder """
22
33import base64
4- import json
54from pathlib import Path
5+ from functools import partial
66
77from docutils import nodes
8- from docutils .parsers .rst import Directive , directives
8+ from docutils .parsers .rst import directives
99from pkg_resources import resource_filename
1010from pygments .lexers import get_all_lexers
11+ from sphinx .highlighting import lexer_classes
1112from sphinx .util .osutil import copyfile
1213from sphinx .util import logging
14+ from sphinx .util .docutils import SphinxDirective
15+ from sphinx .directives .code import CodeBlock
1316
1417
1518FILES = [
@@ -43,44 +46,43 @@ def get_compatible_builders(app):
4346 return builders
4447
4548
46- class TabsDirective (Directive ):
49+ class TabsDirective (SphinxDirective ):
4750 """ Top-level tabs directive """
4851
4952 has_content = True
5053
5154 def run (self ):
5255 """ Parse a tabs directive """
5356 self .assert_has_content ()
54- env = self .state .document .settings .env
5557
5658 node = nodes .container ()
5759 node ["classes" ] = ["sphinx-tabs" ]
5860
59- if "next_tabs_id" not in env .temp_data :
60- env .temp_data ["next_tabs_id" ] = 0
61- if "tabs_stack" not in env .temp_data :
62- env .temp_data ["tabs_stack" ] = []
61+ if "next_tabs_id" not in self . env .temp_data :
62+ self . env .temp_data ["next_tabs_id" ] = 0
63+ if "tabs_stack" not in self . env .temp_data :
64+ self . env .temp_data ["tabs_stack" ] = []
6365
64- tabs_id = env .temp_data ["next_tabs_id" ]
66+ tabs_id = self . env .temp_data ["next_tabs_id" ]
6567 tabs_key = "tabs_%d" % tabs_id
66- env .temp_data ["next_tabs_id" ] += 1
67- env .temp_data ["tabs_stack" ].append (tabs_id )
68+ self . env .temp_data ["next_tabs_id" ] += 1
69+ self . env .temp_data ["tabs_stack" ].append (tabs_id )
6870
69- env .temp_data [tabs_key ] = {}
70- env .temp_data [tabs_key ]["tab_ids" ] = []
71- env .temp_data [tabs_key ]["tab_titles" ] = []
72- env .temp_data [tabs_key ]["is_first_tab" ] = True
71+ self . env .temp_data [tabs_key ] = {}
72+ self . env .temp_data [tabs_key ]["tab_ids" ] = []
73+ self . env .temp_data [tabs_key ]["tab_titles" ] = []
74+ self . env .temp_data [tabs_key ]["is_first_tab" ] = True
7375
7476 self .state .nested_parse (self .content , self .content_offset , node )
7577
76- if env .app .builder .name in get_compatible_builders (env .app ):
78+ if self . env .app .builder .name in get_compatible_builders (self . env .app ):
7779 tabs_node = nodes .container ()
7880 tabs_node .tagname = "div"
7981
8082 classes = "ui top attached tabular menu sphinx-menu"
8183 tabs_node ["classes" ] = classes .split (" " )
8284
83- tab_titles = env .temp_data [tabs_key ]["tab_titles" ]
85+ tab_titles = self . env .temp_data [tabs_key ]["tab_titles" ]
8486 for idx , [data_tab , tab_name ] in enumerate (tab_titles ):
8587 tab = nodes .container ()
8688 tab .tagname = "a"
@@ -91,69 +93,65 @@ def run(self):
9193
9294 node .children .insert (0 , tabs_node )
9395
94- env .temp_data ["tabs_stack" ].pop ()
96+ self . env .temp_data ["tabs_stack" ].pop ()
9597 return [node ]
9698
9799
98- class TabDirective (Directive ):
100+ class TabDirective (SphinxDirective ):
99101 """ Tab directive, for adding a tab to a collection of tabs """
100102
101103 has_content = True
102104
105+ def __init__ (self , * args , ** kwargs ):
106+ self .tab_id = None
107+ self .tab_classes = set ()
108+ super ().__init__ (* args , ** kwargs )
109+
103110 def run (self ):
104111 """ Parse a tab directive """
105112 self .assert_has_content ()
106- env = self .state .document .settings .env
107113
108- tabs_id = env .temp_data ["tabs_stack" ][- 1 ]
114+ tabs_id = self . env .temp_data ["tabs_stack" ][- 1 ]
109115 tabs_key = "tabs_%d" % tabs_id
110116
111- args = self .content [0 ].strip ()
112- if args .startswith ("{" ):
113- try :
114- args = json .loads (args )
115- self .content .trim_start (1 )
116- except ValueError :
117- args = {}
117+ include_tabs_id_in_data_tab = False
118+ if self .tab_id is None :
119+ tab_id = self .env .new_serialno (tabs_key )
120+ include_tabs_id_in_data_tab = True
118121 else :
119- args = {}
122+ tab_id = self . tab_id
120123
121124 tab_name = nodes .container ()
122125 self .state .nested_parse (self .content [:1 ], self .content_offset , tab_name )
123- args ["tab_name" ] = tab_name
124126
125- include_tabs_id_in_data_tab = False
126- if "tab_id" not in args :
127- args ["tab_id" ] = env .new_serialno (tabs_key )
128- include_tabs_id_in_data_tab = True
129127 i = 1
130- while args [ " tab_id" ] in env .temp_data [tabs_key ]["tab_ids" ]:
131- args [ " tab_id" ] = "%s-%d" % (args [ " tab_id" ] , i )
128+ while tab_id in self . env .temp_data [tabs_key ]["tab_ids" ]:
129+ tab_id = "%s-%d" % (tab_id , i )
132130 i += 1
133- env .temp_data [tabs_key ]["tab_ids" ].append (args [ " tab_id" ] )
131+ self . env .temp_data [tabs_key ]["tab_ids" ].append (tab_id )
134132
135- data_tab = str (args [ " tab_id" ] )
133+ data_tab = str (tab_id )
136134 if include_tabs_id_in_data_tab :
137135 data_tab = "%d-%s" % (tabs_id , data_tab )
138136 data_tab = "sphinx-data-tab-{}" .format (data_tab )
139137
140- env .temp_data [tabs_key ]["tab_titles" ].append ((data_tab , args [ " tab_name" ] ))
138+ self . env .temp_data [tabs_key ]["tab_titles" ].append ((data_tab , tab_name ))
141139
142140 text = "\n " .join (self .content )
143141 node = nodes .container (text )
144142
145143 classes = "ui bottom attached sphinx-tab tab segment"
146144 node ["classes" ] = classes .split (" " )
147- node ["classes" ].extend (args . get ( "classes" , []) )
145+ node ["classes" ].extend (self . tab_classes )
148146 node ["classes" ].append (data_tab )
149147
150- if env .temp_data [tabs_key ]["is_first_tab" ]:
148+ if self . env .temp_data [tabs_key ]["is_first_tab" ]:
151149 node ["classes" ].append ("active" )
152- env .temp_data [tabs_key ]["is_first_tab" ] = False
150+ self . env .temp_data [tabs_key ]["is_first_tab" ] = False
153151
154152 self .state .nested_parse (self .content [2 :], self .content_offset , node )
155153
156- if env .app .builder .name not in get_compatible_builders (env .app ):
154+ if self . env .app .builder .name not in get_compatible_builders (self . env .app ):
157155 outer_node = nodes .container ()
158156 tab = nodes .container ()
159157 tab .tagname = "a"
@@ -167,84 +165,66 @@ def run(self):
167165 return [node ]
168166
169167
170- class GroupTabDirective (Directive ):
168+ class GroupTabDirective (TabDirective ):
171169 """ Tab directive that toggles with same tab names across page"""
172170
173171 has_content = True
174172
175173 def run (self ):
176- """ Parse a tab directive """
177174 self .assert_has_content ()
178-
179175 group_name = self .content [0 ]
180- self .content .trim_start (2 )
181-
182- for idx , line in enumerate (self .content .data ):
183- self .content .data [idx ] = " " + line
184-
185- tab_args = {
186- "tab_id" : base64 .b64encode (group_name .encode ("utf-8" )).decode ("utf-8" ),
187- "group_tab" : True ,
188- }
176+ if self .tab_id is None :
177+ self .tab_id = base64 .b64encode (group_name .encode ("utf-8" )).decode ("utf-8" )
178+ return super ().run ()
189179
190- new_content = [
191- ".. tab:: {}" .format (json .dumps (tab_args )),
192- " {}" .format (group_name ),
193- "" ,
194- ]
195180
196- for idx , line in enumerate (new_content ):
197- self .content .data .insert (idx , line )
198- self .content .items .insert (idx , (None , idx ))
199-
200- node = nodes .container ()
201- self .state .nested_parse (self .content , self .content_offset , node )
202- return node .children
203-
204-
205- class CodeTabDirective (Directive ):
181+ class CodeTabDirective (GroupTabDirective ):
206182 """ Tab directive with a codeblock as its content"""
207183
208184 has_content = True
209- option_spec = {"linenos" : directives .flag }
185+ required_arguments = 1 # Lexer name
186+ optional_arguments = 1 # Custom label
187+ final_argument_whitespace = True
188+ option_spec = { # From sphinx CodeBlock
189+ "force" : directives .flag ,
190+ "linenos" : directives .flag ,
191+ "dedent" : int ,
192+ "lineno-start" : int ,
193+ "emphasize-lines" : directives .unchanged_required ,
194+ "caption" : directives .unchanged_required ,
195+ "class" : directives .class_option ,
196+ "name" : directives .unchanged ,
197+ }
210198
211199 def run (self ):
212- """ Parse a tab directive """
200+ """ Parse a code- tab directive"""
213201 self .assert_has_content ()
214202
215- args = self .content [0 ].strip ().split ()
216- self .content .trim_start (2 )
217-
218- lang = args [0 ]
219- tab_name = " " .join (args [1 :]) if len (args ) > 1 else LEXER_MAP [lang ]
220-
221- for idx , line in enumerate (self .content .data ):
222- self .content .data [idx ] = " " + line
223-
224- tab_args = {
225- "tab_id" : base64 .b64encode (tab_name .encode ("utf-8" )).decode ("utf-8" ),
226- "classes" : ["code-tab" ],
227- }
203+ if len (self .arguments ) > 1 :
204+ tab_name = self .arguments [1 ]
205+ elif self .arguments [0 ] in lexer_classes and not isinstance (
206+ lexer_classes [self .arguments [0 ]], partial
207+ ):
208+ tab_name = lexer_classes [self .arguments [0 ]].name
209+ else :
210+ try :
211+ tab_name = LEXER_MAP [self .arguments [0 ]]
212+ except :
213+ raise ValueError ("Lexer not implemented: {}" .format (self .arguments [0 ]))
228214
229- new_content = [
230- ".. tab:: {}" .format (json .dumps (tab_args )),
231- " {}" .format (tab_name ),
232- "" ,
233- " .. code-block:: {}" .format (lang ),
234- ]
215+ self .tab_classes .add ("code-tab" )
235216
236- if "linenos" in self . options :
237- new_content . append ( " :linenos:" )
217+ # All content should be parsed as code
218+ code_block = CodeBlock . run ( self )
238219
239- new_content .append ("" )
220+ # Reset to generate tab node
221+ self .content .data = [tab_name , "" ]
222+ self .content .items = [(None , 0 ), (None , 1 )]
240223
241- for idx , line in enumerate (new_content ):
242- self .content .data .insert (idx , line )
243- self .content .items .insert (idx , (None , idx ))
224+ node = super ().run ()
225+ node [0 ].extend (code_block )
244226
245- node = nodes .container ()
246- self .state .nested_parse (self .content , self .content_offset , node )
247- return node .children
227+ return node
248228
249229
250230class _FindTabsDirectiveVisitor (nodes .NodeVisitor ):
0 commit comments