4. ParsonsDirective Walkthrough
This document explains how the custom ParsonsDirective works in Sphinx/Docutils.
4.1. Directive Definition
class ParsonsDirective(Directive):
has_content = True
optional_arguments = 0
option_spec = {
"title": directives.unchanged,
"shuffle": directives.flag,
"shuffle-js": directives.flag,
"columns": directives.positive_int,
"labels": directives.unchanged,
}
4.2. Options
title: Custom puzzle title (string).shuffle: Shuffle lines server-side (Python).shuffle-js: Shuffle lines client-side (JavaScript).columns: Number of target columns (integer).labels: Comma-separated labels for columns.
4.3. Input Handling
Options Parsing
title = self.options.get("title", "Parsons Puzzle") shuffle = "shuffle" in self.options shuffle_js = "shuffle-js" in self.options columns = int(self.options.get("columns", 1))
Labels
labels_opt = self.options.get("labels") labels = [lbl.strip() for lbl in labels_opt.split(",")] if labels_opt else []
Expected Order
expected_order = [] for line in self.content: if not line.strip(): continue if line.strip().startswith("- "): raw = line.strip()[2:] else: raw = line.strip() indent = len(line) - len(line.lstrip(" ")) expected_order.append((indent, raw))
Line Metadata
lines = [(indent, code, idx+1) for idx, (indent, code) in enumerate(expected_order)]
Shuffle Option
if shuffle: random.shuffle(lines)
4.4. Data Attributes for JS
Expected Solution
expected_attr = "|".join(f"{indent}::{code}" for indent, code in expected_order)
Shuffle Flag
shuffle_attr = "true" if shuffle_js else "false"
4.5. HTML Node Construction
Container ``<div>``
open_div = nodes.raw( "", f'<div class="parsons-container parsons-cols-{columns}" ' f'data-expected="{expected_attr}" data-shuffle-js="{shuffle_attr}">', format="html", )
Title
title_para = nodes.paragraph() title_para += nodes.strong(text=title)
Source List (draggable lines)
source_ul = nodes.bullet_list(classes=["parsons-source"])
Each line becomes an
<li>with:data-line(original line number)data-text(cleaned code)A visible label and
<pre>block
Helper function:
def strip_number_prefix(s: str) -> str: if "|" in s: left, right = s.split("|", 1) if left.strip().isdigit(): return right.strip() return s
Example HTML:
<li class="parsons-line draggable" data-line="3" data-text="print('Hello')"> <span class="line-label">3 |</span> <pre class="no-copybutton no-lineno">print('Hello')</pre> </li>
Target Columns
target_wrapper = nodes.container(classes=["parsons-target-wrapper"]) for c in range(columns): col = nodes.container(classes=["parsons-target", f"parsons-col-{c+1}"]) label_text = labels[c] if c < len(labels) else f"Column {c+1}" label = nodes.paragraph(text=label_text, classes=["parsons-target-label"]) target_ul = nodes.bullet_list(classes=["parsons-target-list"]) target_ul["data-indent"] = str(c) col += label col += target_ul target_wrapper += col
Controls
controls = nodes.raw( "", '<div class="parsons-controls">' '<button class="parsons-check">Check</button>' '<button class="parsons-reset">Reset</button>' '</div>', format="html", )
Closing Div
close_div = nodes.raw("", "</div>", format="html")
4.6. Return Value
return [open_div, title_para, source_ul, target_wrapper, controls, close_div]
4.7. Setup Function
def setup(app):
app.add_directive("parsons", ParsonsDirective)
app.add_css_file("parsons/parsons.css")
app.add_js_file("parsons/parsons.js")
return {
"version": "0.3",
"parallel_read_safe": True,
"parallel_write_safe": True,
}
4.8. Summary of Flow
User writes:
.. parsons:: :title: Example Puzzle :shuffle: :columns: 2 :labels: Left, Right - print("Hello") - x = 5 - if x > 3: - print("Big")
Directive parses options and content.
Builds HTML with draggable
<li>items.Encodes correct solution in
data-expected.Provides target columns and control buttons.
JavaScript handles drag/drop, checking, and resetting.