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

  1. 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))
    
  2. Labels

    labels_opt = self.options.get("labels")
    labels = [lbl.strip() for lbl in labels_opt.split(",")] if labels_opt else []
    
  3. 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))
    
  4. Line Metadata

    lines = [(indent, code, idx+1) for idx, (indent, code) in enumerate(expected_order)]
    
  5. 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

  1. 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",
    )
    
  2. Title

    title_para = nodes.paragraph()
    title_para += nodes.strong(text=title)
    
  3. 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>
    
  4. 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
    
  5. Controls

    controls = nodes.raw(
        "",
        '<div class="parsons-controls">'
        '<button class="parsons-check">Check</button>'
        '<button class="parsons-reset">Reset</button>'
        '</div>',
        format="html",
    )
    
  6. 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

  1. User writes:

    .. parsons::
       :title: Example Puzzle
       :shuffle:
       :columns: 2
       :labels: Left, Right
    
       - print("Hello")
       - x = 5
       - if x > 3:
       -     print("Big")
    
  2. Directive parses options and content.

  3. Builds HTML with draggable <li> items.

  4. Encodes correct solution in data-expected.

  5. Provides target columns and control buttons.

  6. JavaScript handles drag/drop, checking, and resetting.