sphinx-filter-tabs 1.2.2__py3-none-any.whl → 1.2.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
filter_tabs/extension.py CHANGED
@@ -419,12 +419,31 @@ def depart_summary_node(self: HTML5Translator, node: SummaryNode) -> None:
419
419
  self.body.append('</summary>')
420
420
 
421
421
 
422
+ # =============================================================================
423
+ # Use ARIA to Improve Strong Element Behavior
424
+ # =============================================================================
425
+
426
+ def improve_inline_formatting(app: Sphinx, doctree: nodes.document, docname: str):
427
+ """Improve screen reader handling of inline formatting."""
428
+ if app.builder.name != 'html':
429
+ return
430
+
431
+ # Find all strong/emphasis nodes and add ARIA attributes
432
+ for node in doctree.findall(nodes.strong):
433
+ # Add aria-label to make the content read as one unit
434
+ text_content = node.astext()
435
+ node['aria-label'] = text_content
436
+
437
+ for node in doctree.findall(nodes.emphasis):
438
+ text_content = node.astext()
439
+ node['aria-label'] = text_content
440
+
422
441
  # =============================================================================
423
442
  # Static File Handling
424
443
  # =============================================================================
425
444
 
426
445
  def copy_static_files(app: Sphinx):
427
- """Copy CSS and JS files to the build directory."""
446
+ """Copy CSS file to the build directory."""
428
447
  if app.builder.name != 'html':
429
448
  return
430
449
 
@@ -436,11 +455,7 @@ def copy_static_files(app: Sphinx):
436
455
  css_file = static_source_dir / "filter_tabs.css"
437
456
  if css_file.exists():
438
457
  shutil.copy(css_file, dest_dir)
439
-
440
- # Copy JS file if it exists
441
- js_file = static_source_dir / "filter_tabs.js"
442
- if js_file.exists():
443
- shutil.copy(js_file, dest_dir)
458
+
444
459
 
445
460
 
446
461
  # =============================================================================
@@ -456,7 +471,6 @@ def setup(app: Sphinx) -> Dict[str, Any]:
456
471
 
457
472
  # Add static files
458
473
  app.add_css_file('filter_tabs.css')
459
- app.add_js_file('filter_tabs.js')
460
474
 
461
475
  # Register custom nodes (keep existing node registration code)
462
476
  app.add_node(ContainerNode, html=(visit_container_node, depart_container_node))
@@ -475,6 +489,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
475
489
  # Connect event handlers
476
490
  app.connect('builder-inited', copy_static_files)
477
491
  app.connect('doctree-resolved', setup_collapsible_admonitions)
492
+ app.connect('doctree-resolved', improve_inline_formatting)
478
493
 
479
494
  return {
480
495
  'version': __version__,
filter_tabs/renderer.py CHANGED
@@ -46,31 +46,31 @@ class ContentTypeInferrer:
46
46
  (['development', 'staging', 'production', 'test', 'local'], 'environment'),
47
47
  (['source', 'binary', 'docker', 'manual', 'automatic'], 'installation method'),
48
48
  ]
49
-
49
+
50
50
  @classmethod
51
51
  def infer_type(cls, tab_names: List[str]) -> str:
52
52
  """
53
53
  Infer content type from a list of tab names.
54
-
54
+
55
55
  Args:
56
56
  tab_names: List of tab names to analyze
57
-
57
+
58
58
  Returns:
59
59
  Inferred content type string (e.g., 'programming language', 'operating system')
60
60
  """
61
61
  lower_names = [name.lower() for name in tab_names]
62
-
62
+
63
63
  # First pass: exact matches
64
64
  for keywords, content_type in cls.PATTERNS:
65
65
  if any(name in keywords for name in lower_names):
66
66
  return content_type
67
-
67
+
68
68
  # Second pass: substring matches
69
69
  for keywords, content_type in cls.PATTERNS:
70
70
  for name in lower_names:
71
71
  if any(keyword in name for keyword in keywords):
72
72
  return content_type
73
-
73
+
74
74
  # Default fallback
75
75
  return 'option'
76
76
 
@@ -92,53 +92,52 @@ class FilterTabsRenderer:
92
92
  self.tab_data = tab_data
93
93
  self.general_content = general_content
94
94
  self.custom_legend = custom_legend
95
-
95
+
96
96
  # 1. Load configuration first
97
97
  self.config = FilterTabsConfig.from_sphinx_config(self.app.config)
98
-
98
+
99
99
  # 2. Safely initialize the counter on the environment if it doesn't exist
100
100
  if not hasattr(self.env, 'filter_tabs_counter'):
101
101
  self.env.filter_tabs_counter = 0
102
-
102
+
103
103
  # 3. Increment the counter for this new tab group
104
104
  self.env.filter_tabs_counter += 1
105
-
105
+
106
106
  # 4. Generate the unique group ID and the ID generator instance
107
107
  self.group_id = f"filter-group-{self.env.filter_tabs_counter}"
108
108
  self.id_gen = IDGenerator(self.group_id)
109
-
109
+
110
110
  # 5. Perform debug logging now that config and group_id are set
111
111
  if self.config.debug_mode:
112
112
  logger.info(f"Initialized new tab group with id: '{self.group_id}'")
113
113
 
114
114
  def render_html(self) -> List[nodes.Node]:
115
- """Render HTML with a browser-compatible CSS approach."""
115
+ """Render HTML with CSS-only approach (no inline styles)."""
116
116
  if self.config.debug_mode:
117
117
  logger.info(f"Rendering filter-tabs group {self.group_id}")
118
118
 
119
- css_node = self._generate_compatible_css()
120
-
121
119
  container_attrs = self._get_container_attributes()
122
120
  container = ContainerNode(**container_attrs)
123
121
 
124
122
  fieldset = self._create_fieldset()
125
123
  container.children = [fieldset]
126
-
127
- return [css_node, container]
124
+
125
+ # FIXED: No more inline CSS generation
126
+ return [container]
128
127
 
129
128
  def render_fallback(self) -> List[nodes.Node]:
130
129
  """Render for non-HTML builders (e.g., LaTeX)."""
131
130
  output_nodes: List[nodes.Node] = []
132
-
131
+
133
132
  if self.general_content:
134
133
  output_nodes.extend(copy.deepcopy(self.general_content))
135
-
134
+
136
135
  for tab in self.tab_data:
137
136
  admonition = nodes.admonition()
138
137
  admonition += nodes.title(text=tab.name)
139
138
  admonition.extend(copy.deepcopy(tab.content))
140
139
  output_nodes.append(admonition)
141
-
140
+
142
141
  return output_nodes
143
142
 
144
143
  def _get_container_attributes(self) -> Dict[str, Any]:
@@ -150,42 +149,29 @@ class FilterTabsRenderer:
150
149
  'style': self.config.to_css_properties()
151
150
  }
152
151
 
153
- def _generate_compatible_css(self) -> nodes.raw:
154
- """Generate CSS using sibling selectors to show/hide panels."""
155
- css_rules = []
156
- for i, tab in enumerate(self.tab_data):
157
- radio_id = self.id_gen.radio_id(i)
158
- panel_id = self.id_gen.panel_id(i)
159
- css_rules.append(
160
- f"#{radio_id}:checked ~ .{SFT_CONTENT} #{panel_id} {{ display: block; }}"
161
- )
162
-
163
- css_content = "\n".join(css_rules)
164
- return nodes.raw(text=f"<style>\n{css_content}\n</style>", format='html')
165
-
166
152
  def _create_fieldset(self) -> FieldsetNode:
167
153
  """Create the main fieldset containing the legend, radio buttons, and panels."""
168
154
  fieldset = FieldsetNode(role="radiogroup")
169
-
155
+
170
156
  fieldset += self._create_legend()
171
-
157
+
172
158
  radio_group = ContainerNode(classes=[SFT_RADIO_GROUP])
173
159
  self._populate_radio_group(radio_group)
174
-
160
+
175
161
  content_area = ContainerNode(classes=[SFT_CONTENT])
176
162
  self._populate_content_area(content_area)
177
-
163
+
178
164
  # This is the fix: place the content_area inside the radio_group.
179
165
  radio_group += content_area
180
-
166
+
181
167
  fieldset += radio_group
182
-
168
+
183
169
  return fieldset
184
170
 
185
171
  def _create_legend(self) -> LegendNode:
186
172
  """Create a meaningful, visible legend for the tab group."""
187
173
  legend = LegendNode(classes=[SFT_LEGEND], ids=[self.id_gen.legend_id()])
188
-
174
+
189
175
  # Use the custom legend if it exists
190
176
  if self.custom_legend:
191
177
  legend_text = self.custom_legend
@@ -194,27 +180,30 @@ class FilterTabsRenderer:
194
180
  tab_names = [tab.name for tab in self.tab_data]
195
181
  content_type = ContentTypeInferrer.infer_type(tab_names)
196
182
  legend_text = f"Choose {content_type}: {', '.join(tab_names)}"
197
-
183
+
198
184
  legend += nodes.Text(legend_text)
199
185
  return legend
200
186
 
201
187
  def _populate_radio_group(self, radio_group: ContainerNode) -> None:
202
188
  """Create and add all radio buttons and labels to the radio group container."""
203
189
  default_index = next((i for i, tab in enumerate(self.tab_data) if tab.is_default), 0)
204
-
190
+
205
191
  for i, tab in enumerate(self.tab_data):
206
192
  radio_group += self._create_radio_button(i, tab, is_checked=(i == default_index))
207
193
  radio_group += self._create_label(i, tab)
208
194
  radio_group += self._create_screen_reader_description(i, tab)
209
195
 
210
196
  def _create_radio_button(self, index: int, tab: TabData, is_checked: bool) -> RadioInputNode:
211
- """Create a single radio button input."""
197
+ """Create a single radio button input with data attribute."""
212
198
  radio = RadioInputNode(
213
199
  classes=['sr-only'],
214
200
  type='radio',
215
201
  name=self.group_id,
216
202
  ids=[self.id_gen.radio_id(index)],
217
- **{'aria-describedby': self.id_gen.desc_id(index)}
203
+ **{
204
+ 'aria-describedby': self.id_gen.desc_id(index),
205
+ 'data-tab-index': str(index) # FIXED: Add data attribute
206
+ }
218
207
  )
219
208
  if tab.aria_label:
220
209
  radio['aria-label'] = tab.aria_label
@@ -236,26 +225,34 @@ class FilterTabsRenderer:
236
225
  return description_node
237
226
 
238
227
  def _populate_content_area(self, content_area: ContainerNode) -> None:
239
- """Create and add all general and tab-specific content panels."""
228
+ """Create and add all general and tab-specific content panels with accessibility enhancements."""
240
229
  if self.general_content:
241
- general_panel = PanelNode(classes=[SFT_PANEL], **{'data-filter': 'General'})
230
+ general_panel = PanelNode(
231
+ classes=[SFT_PANEL],
232
+ **{
233
+ 'data-filter': 'General',
234
+ 'aria-label': 'General information',
235
+ 'role': 'region'
236
+ }
237
+ )
242
238
  general_panel.extend(copy.deepcopy(self.general_content))
243
239
  content_area += general_panel
244
-
240
+
245
241
  for i, tab in enumerate(self.tab_data):
246
242
  content_area += self._create_tab_panel(i, tab)
247
243
 
248
244
  def _create_tab_panel(self, index: int, tab: TabData) -> PanelNode:
249
- """Create a single content panel for a tab."""
245
+ """Create a single content panel for a tab - CSS only version."""
250
246
  panel_attrs = {
251
247
  'classes': [SFT_PANEL],
252
248
  'ids': [self.id_gen.panel_id(index)],
253
- 'role': 'region',
249
+ 'role': 'tabpanel',
254
250
  'aria-labelledby': self.id_gen.radio_id(index),
255
- 'tabindex': '0',
256
- 'data-tab': tab.name.lower().replace(' ', '-')
251
+ 'tabindex': '0', # Keep for keyboard accessibility
252
+ 'data-tab': tab.name.lower().replace(' ', '-'),
253
+ 'data-tab-index': str(index)
257
254
  }
258
255
  panel = PanelNode(**panel_attrs)
259
256
  panel.extend(copy.deepcopy(tab.content))
260
257
  return panel
261
-
258
+
@@ -1,4 +1,4 @@
1
- /* Sphinx Filter Tabs - Simplified Accessibility-First Stylesheet */
1
+ /* Sphinx Filter Tabs - Enhanced CSS with improved accessibility */
2
2
 
3
3
  /* Main container */
4
4
  .sft-container {
@@ -86,7 +86,7 @@
86
86
  /* Content area */
87
87
  .sft-content {
88
88
  padding: 20px;
89
- flex-basis: 100%; /* Add this line */
89
+ flex-basis: 100%;
90
90
  }
91
91
 
92
92
  /* Panels - hidden by default */
@@ -95,11 +95,39 @@
95
95
  outline: none;
96
96
  }
97
97
 
98
- /* Focus styling for panels */
98
+ /* Enhanced focus styling for CSS-only version */
99
99
  .sft-panel:focus {
100
- outline: 2px solid var(--sft-highlight-color, #007bff);
101
- outline-offset: -2px;
100
+ outline: 3px solid var(--sft-highlight-color, #007bff);
101
+ outline-offset: 2px;
102
102
  border-radius: 4px;
103
+ background: rgba(0, 123, 255, 0.05);
104
+ /* Ensure smooth transition */
105
+ transition: background-color 0.2s ease;
106
+ }
107
+
108
+ /* Visual focus indicator for better UX */
109
+ .sft-panel:focus::before {
110
+ content: "→ ";
111
+ color: var(--sft-highlight-color, #007bff);
112
+ font-weight: bold;
113
+ margin-right: 0.5em;
114
+ }
115
+
116
+ /* Improve screen reader flow for inline formatting */
117
+ strong, b, em, i {
118
+ speak: normal;
119
+ }
120
+
121
+ /* Ensure tab panels are easily discoverable when focused */
122
+ .sft-panel[role="tabpanel"]:focus {
123
+ position: relative;
124
+ z-index: 1;
125
+ }
126
+
127
+ /* ENHANCED: Focus styling for content within panels */
128
+ .sft-panel:focus-within {
129
+ outline: 1px solid var(--sft-highlight-color, #007bff);
130
+ outline-offset: -1px;
103
131
  }
104
132
 
105
133
  /* General content panel - always visible */
@@ -110,6 +138,72 @@
110
138
  border-bottom: 1px solid #eee;
111
139
  }
112
140
 
141
+ /* Panel visibility using data attributes */
142
+ .sft-radio-group input[type="radio"][data-tab-index="0"]:checked ~ .sft-content .sft-panel[data-tab-index="0"],
143
+ .sft-radio-group input[type="radio"][data-tab-index="1"]:checked ~ .sft-content .sft-panel[data-tab-index="1"],
144
+ .sft-radio-group input[type="radio"][data-tab-index="2"]:checked ~ .sft-content .sft-panel[data-tab-index="2"],
145
+ .sft-radio-group input[type="radio"][data-tab-index="3"]:checked ~ .sft-content .sft-panel[data-tab-index="3"],
146
+ .sft-radio-group input[type="radio"][data-tab-index="4"]:checked ~ .sft-content .sft-panel[data-tab-index="4"],
147
+ .sft-radio-group input[type="radio"][data-tab-index="5"]:checked ~ .sft-content .sft-panel[data-tab-index="5"],
148
+ .sft-radio-group input[type="radio"][data-tab-index="6"]:checked ~ .sft-content .sft-panel[data-tab-index="6"],
149
+ .sft-radio-group input[type="radio"][data-tab-index="7"]:checked ~ .sft-content .sft-panel[data-tab-index="7"],
150
+ .sft-radio-group input[type="radio"][data-tab-index="8"]:checked ~ .sft-content .sft-panel[data-tab-index="8"],
151
+ .sft-radio-group input[type="radio"][data-tab-index="9"]:checked ~ .sft-content .sft-panel[data-tab-index="9"] {
152
+ display: block;
153
+ }
154
+
155
+ /* ENHANCED: Accessibility improvements for content within panels */
156
+
157
+ /* Make paragraphs within panels more accessible */
158
+ .sft-panel p {
159
+ margin: 0.75em 0;
160
+ line-height: 1.5;
161
+ }
162
+
163
+ .sft-panel p:first-child {
164
+ margin-top: 0;
165
+ }
166
+
167
+ .sft-panel p:last-child {
168
+ margin-bottom: 0;
169
+ }
170
+
171
+ /* Enhanced code block accessibility */
172
+ .sft-panel .highlight pre,
173
+ .sft-panel code {
174
+ /* Ensure code blocks are focusable and readable */
175
+ border-radius: 4px;
176
+ position: relative;
177
+ }
178
+
179
+ .sft-panel .highlight pre:focus,
180
+ .sft-panel code:focus {
181
+ outline: 2px solid var(--sft-highlight-color, #007bff);
182
+ outline-offset: 2px;
183
+ /* Ensure the content is announced to screen readers */
184
+ z-index: 1;
185
+ }
186
+
187
+ /* Make lists more accessible */
188
+ .sft-panel ul,
189
+ .sft-panel ol {
190
+ margin: 0.75em 0;
191
+ padding-left: 2em;
192
+ }
193
+
194
+ .sft-panel li {
195
+ margin: 0.25em 0;
196
+ line-height: 1.4;
197
+ }
198
+
199
+ /* Enhanced blockquote accessibility */
200
+ .sft-panel blockquote {
201
+ margin: 1em 0;
202
+ padding: 0.5em 1em;
203
+ border-left: 4px solid #ddd;
204
+ background: #f9f9f9;
205
+ }
206
+
113
207
  /* Screen reader only content */
114
208
  .sr-only {
115
209
  position: absolute;
@@ -123,6 +217,23 @@
123
217
  border: 0;
124
218
  }
125
219
 
220
+ /* ENHANCED: Skip link for better keyboard navigation */
221
+ .sft-container .skip-to-content {
222
+ position: absolute;
223
+ top: -40px;
224
+ left: 6px;
225
+ background: var(--sft-highlight-color, #007bff);
226
+ color: white;
227
+ padding: 8px;
228
+ text-decoration: none;
229
+ border-radius: 4px;
230
+ z-index: 1000;
231
+ }
232
+
233
+ .sft-container .skip-to-content:focus {
234
+ top: 6px;
235
+ }
236
+
126
237
  /* Collapsible sections */
127
238
  .collapsible-section {
128
239
  border: 1px solid #e0e0e0;
@@ -164,14 +275,22 @@
164
275
  padding: 15px;
165
276
  }
166
277
 
167
- /* High contrast mode support */
278
+ /* ENHANCED: High contrast mode support with better visibility */
168
279
  @media (prefers-contrast: high) {
169
280
  .sft-radio-group input[type="radio"]:checked + label {
170
281
  border-bottom-width: 4px;
282
+ background: #000;
283
+ color: #fff;
171
284
  }
172
285
 
173
286
  .sft-radio-group input[type="radio"]:focus + label {
174
287
  box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.8);
288
+ outline: 3px solid #fff;
289
+ }
290
+
291
+ .sft-panel:focus {
292
+ outline: 3px solid #000;
293
+ background: #fff;
175
294
  }
176
295
  }
177
296
 
@@ -182,3 +301,24 @@
182
301
  transition: none;
183
302
  }
184
303
  }
304
+
305
+ /* ENHANCED: Screen reader specific improvements */
306
+ @media (prefers-reduced-motion: reduce) {
307
+ /* Ensure focus changes are immediate for screen reader users */
308
+ .sft-panel {
309
+ transition: none;
310
+ }
311
+ }
312
+
313
+ /* Force visibility for screen reader testing */
314
+ @media (forced-colors: active) {
315
+ .sft-panel {
316
+ border: 1px solid ButtonText;
317
+ }
318
+
319
+ .sft-radio-group input[type="radio"]:checked + label {
320
+ border: 2px solid Highlight;
321
+ background: Highlight;
322
+ color: HighlightText;
323
+ }
324
+ }
@@ -0,0 +1,267 @@
1
+ Metadata-Version: 2.4
2
+ Name: sphinx-filter-tabs
3
+ Version: 1.2.6
4
+ Summary: A Sphinx extension for accessible, CSS-first filterable content tabs.
5
+ Author-email: Aputsiak Niels Janussen <aputtu+sphinx@gmail.com>
6
+ License: GNU General Public License v3.0
7
+ Project-URL: Homepage, https://github.com/aputtu/sphinx-filter-tabs
8
+ Project-URL: Repository, https://github.com/aputtu/sphinx-filter-tabs.git
9
+ Project-URL: Issues, https://github.com/aputtu/sphinx-filter-tabs/issues
10
+ Keywords: sphinx,extension,tabs,filter,documentation,css-only,accessibility,keyboard-navigation
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Framework :: Sphinx :: Extension
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Documentation :: Sphinx
20
+ Classifier: Topic :: Software Development :: Documentation
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: Sphinx<9.0,>=7.0
25
+ Dynamic: license-file
26
+
27
+ # Sphinx Filter Tabs Extension
28
+
29
+ [![Tests and Docs Deployment](https://github.com/aputtu/sphinx-filter-tabs/actions/workflows/test.yml/badge.svg)](https://github.com/aputtu/sphinx-filter-tabs/actions/workflows/test.yml)
30
+ [![PyPI version](https://img.shields.io/pypi/v/sphinx-filter-tabs.svg)](https://pypi.org/project/sphinx-filter-tabs/)
31
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/sphinx-filter-tabs.svg)](https://pypi.org/project/sphinx-filter-tabs/)
32
+ [![PyPI - License](https://img.shields.io/pypi/l/sphinx-filter-tabs.svg)](https://github.com/aputtu/sphinx-filter-tabs/blob/main/LICENSE)
33
+
34
+ A robust Sphinx extension for creating accessible, filterable content tabs using pure CSS and semantic HTML.
35
+
36
+ **📖 View extension and documentation at: https://aputtu.github.io/sphinx-filter-tabs/**
37
+
38
+ This extension provides `filter-tabs` and `tab` directives to create user-friendly, switchable content blocks. Perfect for showing code examples in multiple languages, installation instructions for different platforms, or any content that benefits from organized, filterable presentation.
39
+
40
+ ## Key Features
41
+
42
+ - **Pure CSS Implementation:** Zero JavaScript dependencies for maximum compatibility and performance
43
+ - **Fully Accessible:** WAI-ARIA compliant with native keyboard navigation and screen reader support
44
+ - **Semantic HTML:** Uses standard form controls (radio buttons) for robust, predictable behavior
45
+ - **Universal Compatibility:** Works in all environments where CSS is supported, including strict CSP policies
46
+ - **Easy Customization:** Theme colors and styling through simple CSS custom properties
47
+ - **Multiple Output Formats:** Graceful fallback to sequential content in PDF/LaTeX builds
48
+ - **Proven Reliability:** Comprehensive test suite across multiple Python and Sphinx versions
49
+
50
+ ## Quick Start
51
+
52
+ ### Installation
53
+
54
+ ```bash
55
+ pip install sphinx-filter-tabs
56
+ ```
57
+
58
+ ### Enable the Extension
59
+
60
+ Add to your `conf.py`:
61
+
62
+ ```python
63
+ extensions = [
64
+ # ... your other extensions ...
65
+ 'filter_tabs.extension',
66
+ ]
67
+ ```
68
+
69
+ ### Basic Usage
70
+
71
+ ```rst
72
+ .. filter-tabs::
73
+
74
+ This content appears above all tabs.
75
+
76
+ .. tab:: Python
77
+
78
+ Install using pip:
79
+
80
+ .. code-block:: bash
81
+
82
+ pip install my-package
83
+
84
+ .. tab:: Conda (default)
85
+
86
+ Install using conda:
87
+
88
+ .. code-block:: bash
89
+
90
+ conda install my-package
91
+
92
+ .. tab:: From Source
93
+
94
+ Build from source:
95
+
96
+ .. code-block:: bash
97
+
98
+ git clone https://github.com/user/repo.git
99
+ cd repo
100
+ pip install -e .
101
+ ```
102
+
103
+ ## Configuration Options
104
+
105
+ Add these optional settings to your `conf.py`:
106
+
107
+ ```python
108
+ # Customize the active tab highlight color
109
+ filter_tabs_highlight_color = '#007bff' # Default: '#007bff'
110
+
111
+ # Enable debug logging during development
112
+ filter_tabs_debug_mode = False # Default: False
113
+ ```
114
+
115
+ ## Advanced Usage
116
+
117
+ ### Custom Legend
118
+
119
+ Override the auto-generated legend:
120
+
121
+ ```rst
122
+ .. filter-tabs::
123
+ :legend: Select Your Installation Method
124
+
125
+ .. tab:: Quick Install
126
+ Content here...
127
+ ```
128
+
129
+ ### ARIA Labels for Accessibility
130
+
131
+ Provide descriptive labels for screen readers:
132
+
133
+ ```rst
134
+ .. filter-tabs::
135
+
136
+ .. tab:: CLI
137
+ :aria-label: Command line installation instructions
138
+
139
+ Content for command line users...
140
+ ```
141
+
142
+ ### Nested Tabs
143
+
144
+ Create complex layouts with nested tab groups:
145
+
146
+ ```rst
147
+ .. filter-tabs::
148
+
149
+ .. tab:: Windows
150
+
151
+ Choose your package manager:
152
+
153
+ .. filter-tabs::
154
+
155
+ .. tab:: Chocolatey
156
+ choco install my-package
157
+
158
+ .. tab:: Scoop
159
+ scoop install my-package
160
+ ```
161
+
162
+ ## How It Works
163
+
164
+ This extension uses a **pure CSS architecture** with semantic HTML:
165
+
166
+ - **Radio buttons** provide the selection mechanism (hidden but accessible)
167
+ - **CSS `:checked` selectors** control panel visibility
168
+ - **Fieldset/legend** structure provides semantic grouping
169
+ - **ARIA attributes** enhance screen reader support
170
+ - **Native keyboard navigation** works through standard form controls
171
+
172
+ This approach ensures:
173
+ - **Maximum compatibility** across all browsers and assistive technologies
174
+ - **Better performance** with no JavaScript parsing or execution
175
+ - **Enhanced security** for environments with strict Content Security Policies
176
+ - **Simplified maintenance** with fewer dependencies and potential conflicts
177
+
178
+ ## Browser Support
179
+
180
+ Works in all modern browsers that support:
181
+ - CSS3 selectors (`:checked`, attribute selectors)
182
+ - Basic ARIA attributes
183
+ - HTML5 form elements
184
+
185
+ This includes all browsers from the last 10+ years.
186
+
187
+ ## Development
188
+
189
+ ### Quick Setup
190
+
191
+ ```bash
192
+ git clone https://github.com/aputtu/sphinx-filter-tabs.git
193
+ cd sphinx-filter-tabs
194
+ ./scripts/setup_dev.sh
195
+ ```
196
+
197
+ This creates a virtual environment and builds the documentation.
198
+
199
+ ### Development Commands
200
+
201
+ ```bash
202
+ # Activate virtual environment
203
+ source venv/bin/activate
204
+
205
+ # Run tests
206
+ pytest
207
+
208
+ # Build documentation
209
+ ./scripts/dev.sh html
210
+
211
+ # Run tests across multiple Sphinx versions
212
+ tox
213
+
214
+ # Clean build and start fresh
215
+ ./scripts/dev.sh clean-all
216
+
217
+ # Export project structure for analysis
218
+ ./scripts/export-project.sh
219
+ ```
220
+
221
+ ### Testing
222
+
223
+ The project includes comprehensive tests covering:
224
+ - Basic tab functionality and content visibility
225
+ - Accessibility features and ARIA compliance
226
+ - Nested tabs and complex layouts
227
+ - Multiple output formats (HTML, LaTeX)
228
+ - Cross-browser compatibility
229
+
230
+ Tests run automatically on:
231
+ - Python 3.10, 3.12
232
+ - Sphinx 7.0, 7.4, 8.0, 8.2
233
+ - Multiple operating systems via GitHub Actions
234
+
235
+ ## Architecture
236
+
237
+ The extension consists of three main components:
238
+
239
+ - **`extension.py`** - Sphinx integration, directives, and node definitions
240
+ - **`renderer.py`** - HTML generation and output formatting
241
+ - **`static/filter_tabs.css`** - Pure CSS styling and functionality
242
+
243
+ This clean separation makes the code easy to understand, test, and maintain.
244
+
245
+ ## Contributing
246
+
247
+ Contributions are welcome! Please:
248
+
249
+ 1. Fork the repository
250
+ 2. Create a feature branch
251
+ 3. Add tests for new functionality
252
+ 4. Ensure all tests pass: `pytest`
253
+ 5. Submit a pull request
254
+
255
+ ## License
256
+
257
+ GNU General Public License v3.0. See [LICENSE](LICENSE) for details.
258
+
259
+ ## Changelog
260
+
261
+ See [CHANGELOG.md](docs/changelog.rst) for version history and migration notes.
262
+
263
+ ## Support
264
+
265
+ - **Documentation**: https://aputtu.github.io/sphinx-filter-tabs/
266
+ - **Issues**: https://github.com/aputtu/sphinx-filter-tabs/issues
267
+ - **PyPI**: https://pypi.org/project/sphinx-filter-tabs/
@@ -0,0 +1,10 @@
1
+ filter_tabs/__init__.py,sha256=VPpIhj4HaLeMX7ai7dZFkUm81ii2ePPGjCd9hsMjsN4,397
2
+ filter_tabs/extension.py,sha256=6mna59Qg1zdk6Vn9eXU_CaaQg1yBq4ktx2jx9O01frM,18679
3
+ filter_tabs/renderer.py,sha256=ublmQSoFewdPGRtEO7gLrxGqCZCjlooeom0wudx4phk,10100
4
+ filter_tabs/static/filter_tabs.css,sha256=ZoiSWcn2YBEWgkQ-vPbIPHwQ7s2TG4aUikyxM1A8b9I,7956
5
+ sphinx_filter_tabs-1.2.6.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
6
+ sphinx_filter_tabs-1.2.6.dist-info/METADATA,sha256=Wk2ZurjL5XIgRdHoyqtcjwFBGXft5K0aKIREoZWHAMA,7608
7
+ sphinx_filter_tabs-1.2.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ sphinx_filter_tabs-1.2.6.dist-info/entry_points.txt,sha256=za_bQcueY8AHyq7XnnjkW9X3C-LsZjeERVQ_ds7jV1A,62
9
+ sphinx_filter_tabs-1.2.6.dist-info/top_level.txt,sha256=K0Iy-6EsYYdvlyXdsJT0SQg-BLDBgT5-Y8ZKy5SNAfc,12
10
+ sphinx_filter_tabs-1.2.6.dist-info/RECORD,,
@@ -1,160 +0,0 @@
1
- // Progressive enhancement for keyboard navigation and accessibility.
2
- // This file ensures proper keyboard navigation and focus management,
3
- // while maintaining a CSS-only fallback.
4
-
5
- (function() {
6
- 'use strict';
7
-
8
- // Only enhance if the extension's HTML is present on the page.
9
- if (!document.querySelector('.sft-container')) return;
10
-
11
- /**
12
- * Moves focus to the content panel associated with a given radio button.
13
- * This improves accessibility by directing screen reader users to the new content.
14
- * @param {HTMLInputElement} radio The radio button that was selected.
15
- */
16
- function focusOnPanel(radio) {
17
- if (!radio.checked) return;
18
-
19
- // Derive the panel's ID from the radio button's ID.
20
- // e.g., 'filter-group-1-radio-0' becomes 'filter-group-1-panel-0'
21
- const panelId = radio.id.replace('-radio-', '-panel-');
22
- const panel = document.getElementById(panelId);
23
-
24
- if (panel) {
25
- panel.focus();
26
- }
27
- }
28
-
29
- /**
30
- * Creates or updates a live region to announce tab changes to screen readers.
31
- * @param {string} tabName The name of the selected tab.
32
- */
33
- function announceTabChange(tabName) {
34
- // Create or find the live region for screen reader announcements.
35
- let liveRegion = document.getElementById('tab-live-region');
36
- if (!liveRegion) {
37
- liveRegion = document.createElement('div');
38
- liveRegion.id = 'tab-live-region';
39
- liveRegion.setAttribute('role', 'status');
40
- liveRegion.setAttribute('aria-live', 'polite');
41
- liveRegion.setAttribute('aria-atomic', 'true');
42
- // Hide the element visually but keep it accessible.
43
- liveRegion.style.position = 'absolute';
44
- liveRegion.style.left = '-10000px';
45
- liveRegion.style.width = '1px';
46
- liveRegion.style.height = '1px';
47
- liveRegion.style.overflow = 'hidden';
48
- document.body.appendChild(liveRegion);
49
- }
50
-
51
- // Update the announcement text.
52
- liveRegion.textContent = `${tabName} tab selected`;
53
-
54
- // Clear the announcement after a short delay to prevent clutter.
55
- setTimeout(() => {
56
- liveRegion.textContent = '';
57
- }, 1000);
58
- }
59
-
60
- /**
61
- * Initializes keyboard navigation for all filter-tab components on the page.
62
- */
63
- function initTabKeyboardNavigation() {
64
- const containers = document.querySelectorAll('.sft-container');
65
-
66
- containers.forEach(container => {
67
- const tabBar = container.querySelector('.sft-radio-group');
68
- if (!tabBar) return;
69
-
70
- const radios = tabBar.querySelectorAll('input[type="radio"]');
71
- const labels = tabBar.querySelectorAll('label');
72
-
73
- if (radios.length === 0 || labels.length === 0) return;
74
-
75
- // Make labels focusable to act as keyboard navigation targets.
76
- labels.forEach(label => {
77
- if (!label.hasAttribute('tabindex')) {
78
- label.setAttribute('tabindex', '0');
79
- }
80
- });
81
-
82
- // Handle keyboard navigation on the tab labels.
83
- labels.forEach((label, index) => {
84
- label.addEventListener('keydown', (event) => {
85
- let targetIndex = index;
86
- let handled = false;
87
-
88
- switch (event.key) {
89
- case 'ArrowRight':
90
- event.preventDefault();
91
- targetIndex = (index + 1) % labels.length;
92
- handled = true;
93
- break;
94
-
95
- case 'ArrowLeft':
96
- event.preventDefault();
97
- targetIndex = (index - 1 + labels.length) % labels.length;
98
- handled = true;
99
- break;
100
-
101
- case 'Home':
102
- event.preventDefault();
103
- targetIndex = 0;
104
- handled = true;
105
- break;
106
-
107
- case 'End':
108
- event.preventDefault();
109
- targetIndex = labels.length - 1;
110
- handled = true;
111
- break;
112
-
113
- case 'Enter':
114
- case ' ':
115
- // Activate the associated radio button on Enter/Space.
116
- event.preventDefault();
117
- if (radios[index]) {
118
- radios[index].checked = true;
119
- radios[index].dispatchEvent(new Event('change', { bubbles: true }));
120
- }
121
- return;
122
-
123
- default:
124
- return;
125
- }
126
-
127
- if (handled) {
128
- // Move focus to the target label and activate its radio button.
129
- labels[targetIndex].focus();
130
- if (radios[targetIndex]) {
131
- radios[targetIndex].checked = true;
132
- radios[targetIndex].dispatchEvent(new Event('change', { bubbles: true }));
133
- }
134
- }
135
- });
136
- });
137
-
138
- // Add listeners for announcements and focus management.
139
- radios.forEach((radio, index) => {
140
- radio.addEventListener('change', () => {
141
- if (radio.checked) {
142
- // Announce the change to screen readers.
143
- if (labels[index]) {
144
- announceTabChange(labels[index].textContent.trim());
145
- }
146
- // Move focus to the newly visible panel.
147
- focusOnPanel(radio);
148
- }
149
- });
150
- });
151
- });
152
- }
153
-
154
- // Initialize the script once the DOM is ready.
155
- if (document.readyState === 'loading') {
156
- document.addEventListener('DOMContentLoaded', initTabKeyboardNavigation);
157
- } else {
158
- initTabKeyboardNavigation();
159
- }
160
- })();
@@ -1,70 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: sphinx-filter-tabs
3
- Version: 1.2.2
4
- Summary: A Sphinx extension for accessible, CSS-first filterable content tabs.
5
- Author-email: Aputsiak Niels Janussen <aputtu+sphinx@gmail.com>
6
- License: GNU General Public License v3.0
7
- Project-URL: Homepage, https://github.com/aputtu/sphinx-filter-tabs
8
- Project-URL: Repository, https://github.com/aputtu/sphinx-filter-tabs.git
9
- Project-URL: Issues, https://github.com/aputtu/sphinx-filter-tabs/issues
10
- Keywords: sphinx,extension,tabs,filter,documentation,css-only,accessibility,keyboard-navigation
11
- Classifier: Development Status :: 5 - Production/Stable
12
- Classifier: Framework :: Sphinx :: Extension
13
- Classifier: Intended Audience :: Developers
14
- Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
15
- Classifier: Operating System :: OS Independent
16
- Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.10
18
- Classifier: Programming Language :: Python :: 3.12
19
- Classifier: Topic :: Documentation :: Sphinx
20
- Classifier: Topic :: Software Development :: Documentation
21
- Requires-Python: >=3.10
22
- Description-Content-Type: text/markdown
23
- License-File: LICENSE
24
- Requires-Dist: Sphinx<9.0,>=7.0
25
- Dynamic: license-file
26
-
27
- # Sphinx Filter Tabs Extension
28
-
29
- [![Tests and Docs Deployment](https://github.com/aputtu/sphinx-filter-tabs/actions/workflows/test.yml/badge.svg)](https://github.com/aputtu/sphinx-filter-tabs/actions/workflows/test.yml)
30
- [![PyPI version](https://img.shields.io/pypi/v/sphinx-filter-tabs.svg)](https://pypi.org/project/sphinx-filter-tabs/)
31
- [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/sphinx-filter-tabs.svg)](https://pypi.org/project/sphinx-filter-tabs/)
32
- [![PyPI - License](https://img.shields.io/pypi/l/sphinx-filter-tabs.svg)](https://github.com/aputtu/sphinx-filter-tabs/blob/main/LICENSE)
33
-
34
- A robust Sphinx extension for creating accessible, JavaScript-free, filterable content tabs.
35
-
36
- **📖 View extension and documentation at: https://aputtu.github.io/sphinx-filter-tabs/**
37
-
38
- This extension provides `filter-tabs` and `tab` directives to create user-friendly, switchable content blocks, ideal for showing code examples in multiple languages or instructions for different platforms.
39
-
40
- ## Features
41
-
42
- - **No JavaScript:** Pure CSS implementation ensures maximum compatibility, speed, and accessibility.
43
- - **WAI-ARIA Compliant:** The generated HTML follows accessibility best practices for keyboard navigation and screen readers.
44
- - **Highly Customizable:** Easily theme colors, fonts, and sizes directly from your `conf.py` using CSS Custom Properties.
45
- - **Graceful Fallback:** Renders content as simple admonitions in non-HTML outputs like PDF/LaTeX.
46
- - **Automated Testing:** CI/CD pipeline tests against multiple Sphinx versions to ensure compatibility.
47
-
48
- ## Installation
49
-
50
- You can install this extension using `pip`:
51
- ```bash
52
- pip install sphinx-filter-tabs
53
- ```
54
-
55
- ## Development
56
-
57
- 1. You can install a local version of the Sphinx with extension using:
58
- ```bash
59
- ./scripts/setup_dev.sh # Initially cleans previous folders in _docs/build and venv.
60
- ```
61
-
62
- Command to enter venv is provided.
63
-
64
- 2. Once inside virtual environment, you can use following commands:
65
- ```bash
66
- pytest # Runs test suite on configured version of Sphinx.
67
- tox # Check across multiple Sphinx versions. Manual install of tox required.
68
- ./scripts/export-project.sh # Outputs directory structure and code to txt
69
- ./dev.sh [options] # Allows for faster generation for html, pdf, clean up
70
- ```
@@ -1,11 +0,0 @@
1
- filter_tabs/__init__.py,sha256=VPpIhj4HaLeMX7ai7dZFkUm81ii2ePPGjCd9hsMjsN4,397
2
- filter_tabs/extension.py,sha256=Dspt9r8TVoYChHFvGsncuV01GEga7BOOsPaMwYdMQcg,18014
3
- filter_tabs/renderer.py,sha256=e_nUV67ojDHfEXWB7jFkclzy4fOLotluKBg573fGng8,10436
4
- filter_tabs/static/filter_tabs.css,sha256=V7y8BCp0UTQ1KXZNeIYtfz0Zt6b-uJPKYeXcx4PHz4A,3913
5
- filter_tabs/static/filter_tabs.js,sha256=-d_TJREyNxu9TtQNDLvQ0G5qCVWR2TxwrWmApGs2CBo,6559
6
- sphinx_filter_tabs-1.2.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
7
- sphinx_filter_tabs-1.2.2.dist-info/METADATA,sha256=qX7ycCD1JfwXgju5W5pnlhoN1p09cnR_U9yUQZrFpBk,3483
8
- sphinx_filter_tabs-1.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
- sphinx_filter_tabs-1.2.2.dist-info/entry_points.txt,sha256=za_bQcueY8AHyq7XnnjkW9X3C-LsZjeERVQ_ds7jV1A,62
10
- sphinx_filter_tabs-1.2.2.dist-info/top_level.txt,sha256=K0Iy-6EsYYdvlyXdsJT0SQg-BLDBgT5-Y8ZKy5SNAfc,12
11
- sphinx_filter_tabs-1.2.2.dist-info/RECORD,,