From d7c4a11520888757a130aaf7425730d041d33d83 Mon Sep 17 00:00:00 2001 From: Eatham532 <78714349+Eatham532@users.noreply.github.com> Date: Tue, 28 Oct 2025 16:00:28 +1100 Subject: [PATCH 1/3] feat: Custom Structure chart init Signed-off-by: Eatham532 <78714349+Eatham532@users.noreply.github.com> --- docs/structure-chart-examples.md | 141 ++++++ extensions/structure_chart.py | 735 +++++++++++++++++++++++++++++++ mkdocs.yml | 5 + test_structure_chart.py | 419 ++++++++++++++++++ 4 files changed, 1300 insertions(+) create mode 100644 docs/structure-chart-examples.md create mode 100644 extensions/structure_chart.py create mode 100644 test_structure_chart.py diff --git a/docs/structure-chart-examples.md b/docs/structure-chart-examples.md new file mode 100644 index 00000000..9623b471 --- /dev/null +++ b/docs/structure-chart-examples.md @@ -0,0 +1,141 @@ +# Structure Chart Examples + +This page demonstrates the custom structure chart syntax and capabilities. + +## Basic Example + +A simple structure chart with a control module and sub-modules: + +```structure-chart +module main "Grade Calculator" + module input "Get Input" + module calc "Calculate Grade" + module output "Display Result" + +main -> input +main -> calc +main -> output +``` + +## With Connection Types + +Shows different types of connections (data flow, control flow, conditional): + +```structure-chart +module main "Process Order" + module validate "Validate Order" + module check "Check Stock" + module process "Process Payment" + module ship "Ship Items" + +main -> validate data "order details" +validate -> check control +validate -> process conditional +check -> ship data "items" +``` + +## Library Modules + +Library modules are shown with an underline and can be called from multiple places: + +```structure-chart +module app "Main App" + module userInput "User Input" + module processData "Process Data" + library logger "Log Message" + +app -> userInput +app -> processData +userInput -> logger data "user action" +processData -> logger data "result" +``` + +## With Loop + +A loop indicator shows repetitive execution: + +```structure-chart +module main "Process Files" + module readFile "Read File" + module parseData "Parse Data" + module saveData "Save Data" + +main -> readFile +readFile -> parseData +parseData -> saveData + +loop over readFile parseData saveData +``` + +## With Storage + +Physical storage elements (databases, files): + +```structure-chart +module app "Student System" + module add "Add Student" + module view "View Students" +storage db "Student Database" + +app -> add +app -> view +add -> db data "student record" +view -> db data "query" +``` + +## Complete Example + +A comprehensive example showing all features: + +```structure-chart +# Grade Management System +module main "Grade Manager" + module input "Input Grades" + module calc "Calculate Average" + module validate "Validate Data" + module report "Generate Report" + library util "Utility Functions" +storage gradeDB "Grade Database" + +main -> input +main -> validate conditional +input -> calc data "grades" +calc -> validate control +validate -> report data "validated grades" +input -> util data "format check" +calc -> util data "math operations" +report -> gradeDB data "save results" + +loop over input calc validate +``` + +## Syntax Reference + +### Modules + +```text +module id "Label" # Regular module +library id "Label" # Library module (underlined, reusable) +storage id "Label" # Physical storage (curved rectangle) +``` + +### Connections + +```text +from -> to # Simple connection +from -> to data "label" # Data flow (empty circle) +from -> to control # Control flow (filled circle) +from -> to conditional # Conditional call (diamond) +``` + +### Loops + +```text +loop over module1 module2 module3 # Curved arrow over modules +``` + +### Layout + +- Indentation (2 spaces per level) determines hierarchy +- Modules at the same indentation appear on the same level +- Parent-child relationships are shown through connections diff --git a/extensions/structure_chart.py b/extensions/structure_chart.py new file mode 100644 index 00000000..3c3dae08 --- /dev/null +++ b/extensions/structure_chart.py @@ -0,0 +1,735 @@ +""" +Custom Structure Chart Extension for MkDocs +Generates SVG structure charts from a simple text-based syntax. + +Structure Chart Elements: +1. Module - Rectangle with name + - Control Module: Branches to multiple submodules + - Sub Module: Child of another module + - Library Module: Reusable, invokable from any module (underlined) +2. Conditional Call - Diamond on connection line +3. Loop - Curved arrow covering multiple modules +4. Data Flow - Arrow with empty circle +5. Control Flow - Arrow with filled circle +6. Physical Storage - Curved rectangle +""" + +from markdown.extensions import Extension +from markdown.preprocessors import Preprocessor +import re +import hashlib +from xml.etree import ElementTree as etree + + +class StructureChartGenerator: + """Generates SVG structure charts from text definitions.""" + + # Layout constants + MODULE_WIDTH = 120 + MODULE_HEIGHT = 50 + MODULE_SPACING_X = 40 + MODULE_SPACING_Y = 80 + LIBRARY_MODULE_COLOR = "#e3f2fd" # Light blue for library modules + MODULE_COLOR = "#ffffff" + STROKE_COLOR = "#333333" + STORAGE_COLOR = "#fff9c4" # Light yellow for storage + + def __init__(self, chart_def: str): + """ + Initialize with chart definition. + + Args: + chart_def: Text definition of the structure chart + """ + self.chart_def = chart_def + self.modules = {} # id -> module data + self.connections = [] # list of connection data + self.conditionals = [] # list of conditional gates (diamond with multiple outputs) + self.loops = [] # list of loop definitions + self.storages = [] # list of storage elements + self.levels = {} # level -> list of module ids + + def parse(self): + """Parse the chart definition.""" + lines = self.chart_def.strip().split('\n') + current_level = 0 + + for line in lines: + line = line.rstrip() + if not line or line.strip().startswith('#'): + continue + + # Calculate indentation level (2 spaces = 1 level) + indent = len(line) - len(line.lstrip()) + level = indent // 2 + content = line.strip() + + # Parse different element types + if content.startswith('module '): + self._parse_module(content, level) + elif content.startswith('library '): + self._parse_library(content, level) + elif content.startswith('storage '): + self._parse_storage(content, level) + elif content.startswith('conditional '): + self._parse_conditional(content) + elif content.startswith('loop '): + self._parse_loop(content) + elif '->' in content: + self._parse_connection(content) + + def _parse_module(self, content: str, level: int): + """Parse a module definition: module id "Label" """ + match = re.match(r'module\s+(\w+)\s+"([^"]+)"', content) + if match: + module_id = match.group(1) + label = match.group(2) + self.modules[module_id] = { + 'id': module_id, + 'label': label, + 'level': level, + 'type': 'module' + } + if level not in self.levels: + self.levels[level] = [] + self.levels[level].append(module_id) + + def _parse_library(self, content: str, level: int): + """Parse a library module: library id "Label" """ + match = re.match(r'library\s+(\w+)\s+"([^"]+)"', content) + if match: + module_id = match.group(1) + label = match.group(2) + self.modules[module_id] = { + 'id': module_id, + 'label': label, + 'level': level, + 'type': 'library' + } + if level not in self.levels: + self.levels[level] = [] + self.levels[level].append(module_id) + + def _parse_storage(self, content: str, level: int): + """Parse a storage element: storage id "Label" """ + match = re.match(r'storage\s+(\w+)\s+"([^"]+)"', content) + if match: + storage_id = match.group(1) + label = match.group(2) + self.storages.append({ + 'id': storage_id, + 'label': label, + 'level': level + }) + + def _parse_conditional(self, content: str): + """ + Parse a conditional gate: conditional from to_id1 to_id2 to_id3 + Example: conditional main optionA optionB optionC + """ + parts = content.split()[1:] # Skip 'conditional' keyword + if len(parts) < 2: + return + + from_id = parts[0] + to_ids = parts[1:] + + self.conditionals.append({ + 'from': from_id, + 'to_list': to_ids + }) + + def _parse_loop(self, content: str): + """Parse a loop definition: loop over id1 id2 id3""" + match = re.match(r'loop\s+over\s+(.+)', content) + if match: + module_ids = match.group(1).split() + self.loops.append(module_ids) + + def _parse_connection(self, content: str): + """ + Parse a connection: from -> to [type] [direction] [label] + Types: data, control, normal + Direction: forward (default), backward (for arrows pointing opposite to connection) + Examples: + a -> b + a -> b data "user input" + a -> b data forward "user input" + a -> b data backward "response" + a -> b control + a -> b control backward + """ + parts = content.split('->') + if len(parts) != 2: + return + + from_id = parts[0].strip() + rest = parts[1].strip().split(None, 3) + + to_id = rest[0] + conn_type = 'normal' + direction = 'forward' # default direction + label = '' + + if len(rest) > 1: + conn_type = rest[1] + if len(rest) > 2: + # Check if next item is direction or label + if rest[2] in ['forward', 'backward']: + direction = rest[2] + if len(rest) > 3: + label = rest[3].strip('"') + else: + label = rest[2].strip('"') + + self.connections.append({ + 'from': from_id, + 'to': to_id, + 'type': conn_type, + 'direction': direction, + 'label': label + }) + + def _calculate_positions(self): + """Calculate x, y positions for all modules based on hierarchy.""" + # Build parent-child relationships from connections AND conditionals + children = {} # parent_id -> list of child_ids + parents = {} # child_id -> parent_id + + for conn in self.connections: + parent = conn['from'] + child = conn['to'] + if parent not in children: + children[parent] = [] + children[parent].append(child) + parents[child] = parent + + # Add conditional connections - children go below the conditional parent + for conditional in self.conditionals: + parent = conditional['from'] + for child in conditional['to_list']: + if parent not in children: + children[parent] = [] + if child not in children[parent]: # Avoid duplicates + children[parent].append(child) + if child not in parents: # Don't override if already has a parent + parents[child] = parent + + # Find root modules (no parent) + roots = [mid for mid in self.modules.keys() if mid not in parents] + + # Position modules using tree layout + self._position_tree(roots, children, 0, 0) + + def _position_tree(self, module_ids, children, x_offset, y_level): + """Recursively position modules in a tree structure.""" + if not module_ids: + return x_offset + + current_x = x_offset + + for module_id in module_ids: + if module_id not in self.modules: + continue + + # Get children of this module + child_ids = children.get(module_id, []) + + if child_ids: + # Position children first to determine width needed + child_start_x = current_x + child_end_x = self._position_tree( + child_ids, + children, + child_start_x, + y_level + 1 + ) + + # Center parent over children + parent_x = (child_start_x + child_end_x - self.MODULE_WIDTH) / 2 + current_x = child_end_x + else: + # Leaf node - just place it + parent_x = current_x + current_x += self.MODULE_WIDTH + self.MODULE_SPACING_X + + # Set position + self.modules[module_id]['x'] = parent_x + self.modules[module_id]['y'] = y_level * (self.MODULE_HEIGHT + self.MODULE_SPACING_Y) + self.modules[module_id]['level'] = y_level + + return current_x + + def _draw_module(self, module: dict) -> str: + """Draw a single module as SVG.""" + x, y = module['x'], module['y'] + label = module['label'] + is_library = module['type'] == 'library' + + fill_color = self.LIBRARY_MODULE_COLOR if is_library else self.MODULE_COLOR + + # Split long labels into multiple lines with better logic + words = label.split() + lines = [] + current_line = [] + max_chars = 16 # Maximum characters per line + + for word in words: + test_line = ' '.join(current_line + [word]) + if len(test_line) > max_chars and current_line: + lines.append(' '.join(current_line)) + current_line = [word] + else: + current_line.append(word) + if current_line: + lines.append(' '.join(current_line)) + + svg = f'\n' + + # Add subtle drop shadow for depth + svg += f' \n' + + # Main module rectangle + svg += f' \n' + + # Add text (centered, with better vertical spacing) + line_height = 14 + total_text_height = len(lines) * line_height + text_start_y = y + (self.MODULE_HEIGHT - total_text_height) / 2 + line_height / 2 + + for i, line in enumerate(lines): + line_y = text_start_y + i * line_height + svg += f' {line}\n' + + # Add underline for library modules (positioned below text) + if is_library: + underline_y = text_start_y + (len(lines) - 1) * line_height + 8 + svg += f' \n' + + svg += '\n' + return svg + + def _draw_connection(self, conn: dict, offset_multiplier: float = 0) -> str: + """Draw a connection between modules. + + Args: + conn: Connection dictionary with from, to, type, etc. + offset_multiplier: Multiplier for offsetting multiple arrows (-1, 0, 1, etc.) + """ + if conn['from'] not in self.modules or conn['to'] not in self.modules: + return '' + + from_module = self.modules[conn['from']] + to_module = self.modules[conn['to']] + + # Tree-like structure: all connections exit from center bottom of parent + from_center_x = from_module['x'] + self.MODULE_WIDTH / 2 + from_bottom_y = from_module['y'] + self.MODULE_HEIGHT + + to_center_x = to_module['x'] + self.MODULE_WIDTH / 2 + to_top_y = to_module['y'] + + # For parent-child relationships, use simple vertical connection from center + x1 = from_center_x + y1 = from_bottom_y + x2 = to_center_x + y2 = to_top_y + + svg = '\n' + + # Add data/control indicators ALONG the line (back-to-back for multiples) + if conn['type'] in ['data', 'control']: + import math + + # Calculate line angle and perpendicular + angle = math.atan2(y2 - y1, x2 - x1) + perp_angle = angle + math.pi / 2 + + # Position indicators ALONG the line at different positions for multiples + # First arrow at 30% along line, second at 50%, third at 70%, etc. + line_positions = [0.3, 0.5, 0.7, 0.85] + position_index = min(int(offset_multiplier), len(line_positions) - 1) + line_position = line_positions[position_index] + + # Calculate position along the line + indicator_center_x = x1 + (x2 - x1) * line_position + indicator_center_y = y1 + (y2 - y1) * line_position + + # Offset slightly to the side so not directly on main line + side_offset = 12 # Small offset to the side + indicator_center_x += math.cos(perp_angle) * side_offset + indicator_center_y += math.sin(perp_angle) * side_offset + + # Arrow properties (length is 2x the arrowhead which is ~10px) + arrow_length = 20 # Total length of the arrow + + # Determine arrow direction (forward = same as line, backward = opposite) + if conn.get('direction', 'forward') == 'backward': + arrow_angle = angle + math.pi # Point opposite direction + else: + arrow_angle = angle # Point same direction as line + + # Calculate arrow positions + circle_x = indicator_center_x - math.cos(arrow_angle) * arrow_length / 2 + circle_y = indicator_center_y - math.sin(arrow_angle) * arrow_length / 2 + arrow_tip_x = indicator_center_x + math.cos(arrow_angle) * arrow_length / 2 + arrow_tip_y = indicator_center_y + math.sin(arrow_angle) * arrow_length / 2 + + # Draw the arrow LINE FIRST (before circles, so circles cover it) + svg += f' \n' + + if conn['type'] == 'data': + # Data flow: Circle with arrow (empty circle with white fill) + # Draw circle AFTER line so it covers the line + svg += f' \n' + + elif conn['type'] == 'control': + # Control flow: Filled circle with arrow (solid circle) + # Draw filled circle AFTER line so it covers the line + svg += f' \n' + + # Draw main connection line (always present) - AFTER arrows so it doesn't cover them + svg += f' \n' + + # Add label if present (positioned beside the arrow along the line) + if conn['label'] and conn['type'] in ['data', 'control']: + import math + # Position label next to the arrow indicator + angle = math.atan2(y2 - y1, x2 - x1) + perp_angle = angle + math.pi / 2 + + # Use same position along line as the arrow + line_positions = [0.3, 0.5, 0.7, 0.85] + position_index = min(int(offset_multiplier), len(line_positions) - 1) + line_position = line_positions[position_index] + + # Position along the line + label_line_x = x1 + (x2 - x1) * line_position + label_line_y = y1 + (y2 - y1) * line_position + + # Estimate text width and position to the side + label_text = conn['label'] + text_width = len(label_text) * 6 + + # Position label to the side (offset from line) + # arrow is at 12px to the side, so label goes further out + label_offset = 12 + 20 + 8 + text_width / 2 + + label_x = label_line_x + math.cos(perp_angle) * label_offset + label_y = label_line_y + math.sin(perp_angle) * label_offset + + svg += f' {label_text}\n' + elif conn['label']: + # For connections without data/control, place label to the side + import math + angle = math.atan2(y2 - y1, x2 - x1) + perp_angle = angle + math.pi / 2 + + mid_x = (x1 + x2) / 2 + mid_y = (y1 + y2) / 2 + label_offset = 15 + label_x = mid_x + math.cos(perp_angle) * label_offset + label_y = mid_y + math.sin(perp_angle) * label_offset + + svg += f' {conn["label"]}\n' + + svg += '\n' + return svg + + def _draw_loop(self, module_ids: list) -> str: + """Draw a simple circular loop indicator beneath the parent module.""" + if not module_ids: + return '' + + # Get positions of all modules in loop + modules = [self.modules[mid] for mid in module_ids if mid in self.modules] + if not modules: + return '' + + # Find the ACTUAL parent module by checking connections + parent_module = None + for conn in self.connections: + if conn['to'] in module_ids: + potential_parent = conn['from'] + if potential_parent not in module_ids and potential_parent in self.modules: + parent_module = self.modules[potential_parent] + break + + # Fallback: if no parent found, use the first module's parent level + if not parent_module: + loop_level = min(m.get('level', 0) for m in modules) + if loop_level > 0: + for mid, m in self.modules.items(): + if m.get('level', 0) == loop_level - 1: + parent_module = m + break + + if not parent_module: + parent_module = modules[0] + + # Calculate parent module bounds + parent_center_x = parent_module['x'] + self.MODULE_WIDTH / 2 + parent_bottom_y = parent_module['y'] + self.MODULE_HEIGHT + + # Larger, more visible circular loop below the parent module + loop_radius = 25 # Increased from 15 for better visibility + loop_center_y = parent_bottom_y + loop_radius + 10 # Moved down more (was +5) + + # Draw a circular arc with arrow pointing back up + svg = '\n' + + # Draw almost a complete circle - start and end near the top + # Start point (left side, near parent) + start_x = parent_center_x - loop_radius + 5 + start_y = parent_bottom_y + 8 + + # End point (right side, near parent - arrow will point here) + end_x = parent_center_x + loop_radius - 5 + end_y = parent_bottom_y + 8 + + # Create a large circular arc that shows most of the circle + svg += f' str: + """Draw a storage element (curved rectangle with database styling).""" + # Position storage elements at the bottom + max_level = max(self.levels.keys()) if self.levels else 0 + x = -self.MODULE_WIDTH / 2 + position * (self.MODULE_WIDTH + self.MODULE_SPACING_X) + y = (max_level + 1) * (self.MODULE_HEIGHT + self.MODULE_SPACING_Y) + + svg = '\n' + + # Add shadow for depth + svg += f' \n' + + # Add a top "cap" line to make it look more like a cylinder/database + cap_height = 8 + svg += f' \n' + + # Add text (centered) + svg += f' {storage["label"]}\n' + svg += '\n' + return svg + + def _draw_conditional(self, conditional: dict) -> str: + """Draw a conditional gate (diamond) with multiple output connections.""" + if conditional['from'] not in self.modules: + return '' + + from_module = self.modules[conditional['from']] + from_x = from_module['x'] + self.MODULE_WIDTH / 2 + from_y = from_module['y'] + self.MODULE_HEIGHT + + # Position diamond below the from module + diamond_size = 20 + diamond_x = from_x + diamond_y = from_y + 30 + + svg = '\n' + + # Add shadow for depth + shadow_offset = 2 + svg += f' \n' + + # Draw line from module to diamond + svg += f' \n' + + # Draw lines from diamond to each destination module + for to_id in conditional['to_list']: + if to_id in self.modules: + to_module = self.modules[to_id] + to_x = to_module['x'] + self.MODULE_WIDTH / 2 + to_y = to_module['y'] + + # Line from diamond to module + svg += f' \n' + + svg += '\n' + return svg + + def generate_svg(self) -> str: + """Generate the complete SVG diagram.""" + self.parse() + self._calculate_positions() + + if not self.modules: + return 'No modules defined' + + # Calculate SVG dimensions + all_x = [m['x'] for m in self.modules.values()] + all_y = [m['y'] for m in self.modules.values()] + + min_x = min(all_x) - 50 + max_x = max(all_x) + self.MODULE_WIDTH + 50 + min_y = min(all_y) - 80 # Extra space for loops + max_y = max(all_y) + self.MODULE_HEIGHT + 50 + + # Add space for storage if present + if self.storages: + max_level = max(self.levels.keys()) if self.levels else 0 + max_y = (max_level + 1) * (self.MODULE_HEIGHT + self.MODULE_SPACING_Y) + self.MODULE_HEIGHT + 50 + + width = max_x - min_x + height = max_y - min_y + + # Build SVG + svg = f'\n' + + # Add definitions for markers + svg += ''' + + + + + + + \n''' + + # Draw loops first (behind everything) + for loop_modules in self.loops: + svg += self._draw_loop(loop_modules) + + # Draw conditional gates + for conditional in self.conditionals: + svg += self._draw_conditional(conditional) + + # Group connections by from/to pair to handle multiple arrows on same line + connection_groups = {} + for i, conn in enumerate(self.connections): + key = (conn['from'], conn['to']) + if key not in connection_groups: + connection_groups[key] = [] + connection_groups[key].append((i, conn)) + + # Draw connections with offsets for duplicates + for (from_id, to_id), conns in connection_groups.items(): + num_conns = len(conns) + for idx, (original_idx, conn) in enumerate(conns): + # Calculate offset index: -1, 0, 1 for 3 connections; -0.5, 0.5 for 2, etc. + if num_conns == 1: + offset_multiplier = 0 + else: + # Center the group: for n items, use positions from -(n-1)/2 to +(n-1)/2 + offset_multiplier = idx - (num_conns - 1) / 2 + + svg += self._draw_connection(conn, offset_multiplier) + + # Draw modules + for module in self.modules.values(): + svg += self._draw_module(module) + + # Draw storage elements + for i, storage in enumerate(self.storages): + svg += self._draw_storage(storage, i) + + svg += '' + return svg + + +class StructureChartPreprocessor(Preprocessor): + """Preprocessor to convert structure-chart fenced blocks to SVG.""" + + def run(self, lines): + new_lines = [] + in_structure_chart = False + chart_lines = [] + + i = 0 + while i < len(lines): + line = lines[i] + + # Check for structure chart fence start + if line.strip() == '```structure-chart': + in_structure_chart = True + chart_lines = [] + i += 1 + continue + + # Check for fence end + if in_structure_chart and line.strip() == '```': + # Generate SVG + chart_def = '\n'.join(chart_lines) + generator = StructureChartGenerator(chart_def) + svg = generator.generate_svg() + + # Generate a unique ID for the diagram + diagram_id = hashlib.md5(chart_def.encode()).hexdigest()[:8] + + # Wrap in diagram container for modal support + new_lines.append(f'
') + new_lines.append(svg) + new_lines.append(f'') + new_lines.append('
') + new_lines.append('') + + in_structure_chart = False + chart_lines = [] + i += 1 + continue + + # Accumulate chart lines or pass through + if in_structure_chart: + chart_lines.append(line) + else: + new_lines.append(line) + + i += 1 + + return new_lines + + +class StructureChartExtension(Extension): + """MkDocs extension for structure charts.""" + + def extendMarkdown(self, md): + md.preprocessors.register( + StructureChartPreprocessor(md), + 'structure_chart', + priority=175 # Before fenced_code + ) + + +def makeExtension(**kwargs): + return StructureChartExtension(**kwargs) diff --git a/mkdocs.yml b/mkdocs.yml index a7732769..96310aa3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -95,6 +95,7 @@ markdown_extensions: - footnotes - extensions.glossary_extension - extensions.kroki_wrapper + - extensions.structure_chart - md_in_html - smarty - tables @@ -107,6 +108,10 @@ markdown_extensions: - pymdownx.tasklist - pymdownx.superfences: custom_fences: + # Structure Charts + - name: structure-chart + class: structure-chart + format: !!python/name:extensions.structure_chart.format_structure_chart # Python - name: python-exec class: python-exec diff --git a/test_structure_chart.py b/test_structure_chart.py new file mode 100644 index 00000000..547e2d6a --- /dev/null +++ b/test_structure_chart.py @@ -0,0 +1,419 @@ +""" +Test script for structure chart generation. +Generates SVG files directly without running MkDocs server. +""" + +import sys +from pathlib import Path + +# Add project root to path to import extensions +project_root = Path(__file__).parent +sys.path.insert(0, str(project_root)) + +from extensions.structure_chart import StructureChartGenerator + + +def save_svg(svg_content: str, filename: str): + """Save SVG to file and print result.""" + output_path = project_root / "test_output" / filename + output_path.parent.mkdir(exist_ok=True) + output_path.write_text(svg_content, encoding='utf-8') + print(f"āœ… Generated: {output_path}") + + +def test_basic(): + """Test basic structure chart.""" + chart_def = """ +module main "Grade Calculator" + module input "Get Input" + module calc "Calculate Grade" + module output "Display Result" + +main -> input +main -> calc +main -> output +""" + generator = StructureChartGenerator(chart_def) + svg = generator.generate_svg() + save_svg(svg, "test_basic.svg") + return svg + + +def test_connection_types(): + """Test different connection types.""" + chart_def = """ +module main "Process Order" + module validate "Validate Order" + module check "Check Stock" + module process "Process Payment" + module ship "Ship Items" + +main -> validate data "order details" +validate -> check control "initiate" +validate -> process data backward "status" +check -> ship data "items" +process -> ship control backward "confirmation" +""" + generator = StructureChartGenerator(chart_def) + svg = generator.generate_svg() + save_svg(svg, "test_connections.svg") + return svg + + +def test_library(): + """Test library modules.""" + chart_def = """ +module app "Main App" + module userInput "User Input" + module processData "Process Data" + library logger "Log Message" + +app -> userInput +app -> processData +userInput -> logger data "user action" +processData -> logger data "result" +""" + generator = StructureChartGenerator(chart_def) + svg = generator.generate_svg() + save_svg(svg, "test_library.svg") + return svg + + +def test_loop(): + """Test loop indicator.""" + chart_def = """ +module main "Process Files" + module readFile "Read File" + module parseData "Parse Data" + module saveData "Save Data" + +main -> readFile +readFile -> parseData +parseData -> saveData + +loop over readFile parseData saveData +""" + generator = StructureChartGenerator(chart_def) + svg = generator.generate_svg() + save_svg(svg, "test_loop.svg") + return svg + + +def test_partial_loop(): + """Test loop over subset of child modules.""" + chart_def = """ +module main "Data Processor" + module fetch "Fetch Data" + module transform "Transform Data" + module validate "Validate Data" + module save "Save Results" + +main -> fetch +main -> transform data "raw data" +main -> validate control "check quality" +main -> save + +loop over fetch transform +""" + generator = StructureChartGenerator(chart_def) + svg = generator.generate_svg() + save_svg(svg, "test_partial_loop.svg") + return svg + + +def test_multiple_arrows(): + """Test multiple data/control flows on same connection.""" + chart_def = """ +module process "Order Processor" + module validate "Validate Order" + module execute "Execute Order" + +process -> validate data "order data" +process -> validate data "customer info" +process -> validate control "validation rules" +validate -> execute data "validated order" +validate -> execute control "proceed signal" +""" + generator = StructureChartGenerator(chart_def) + svg = generator.generate_svg() + save_svg(svg, "test_multiple_arrows.svg") + return svg + + +def test_storage(): + """Test storage elements.""" + chart_def = """ +module app "Student System" + module add "Add Student" + module view "View Students" +storage db "Student Database" + +app -> add +app -> view +add -> db data "student record" +view -> db data "query" +""" + generator = StructureChartGenerator(chart_def) + svg = generator.generate_svg() + save_svg(svg, "test_storage.svg") + return svg + + +def test_conditional_gate(): + """Test conditional gate with multiple branches.""" + chart_def = """ +module main "User Login" + module checkA "Admin Access" + module checkB "User Access" + module checkC "Guest Access" + +conditional main checkA checkB checkC +""" + generator = StructureChartGenerator(chart_def) + svg = generator.generate_svg() + save_svg(svg, "test_conditional.svg") + return svg + + +def test_complete(): + """Test complete example with all features.""" + chart_def = """ +# Grade Management System +module main "Grade Manager" + module input "Input Grades" + module readFile "Read from File" + module keyboard "Keyboard Entry" + module process "Process Grades" + module validate "Validate Data" + module calculate "Calculate Average" + module output "Generate Report" + library logger "Log Events" +storage gradeDB "Grade Database" + +main -> input +main -> process +main -> output +input -> readFile conditional +input -> keyboard conditional +process -> validate control +validate -> calculate data "valid grades" +output -> gradeDB data "save report" +input -> logger data "input event" +process -> logger data "process event" +output -> logger data "output event" +""" + generator = StructureChartGenerator(chart_def) + svg = generator.generate_svg() + save_svg(svg, "test_complete.svg") + return svg + + +def create_html_viewer(): + """Create an HTML file to view all generated SVGs.""" + html = """ + + + + + Structure Chart Test Results + + + +

Structure Chart Test Results

+

āœ… All tests completed successfully!

+ +
+

1. Basic Structure Chart

+

Simple hierarchy with control module and sub-modules.

+
+ +
+
+ +
+

2. Connection Types

+

Different connection types: data flow (empty circle), control flow (filled circle), conditional (diamond).

+
+ +
+
+ +
+

3. Library Module

+

Reusable library module (underlined) called from multiple places.

+
+ +
+
+ +
+

4. Loop Indicator

+

Curved arrow showing repetitive execution over multiple modules.

+
+ +
+
+ +
+

5. Partial Loop (2 of 4 modules)

+

Loop over subset of child modules - demonstrates selective iteration.

+
+ +
+
+ +
+

6. Multiple Arrows (same connection)

+

Multiple data/control flows on the same connection line appear side-by-side.

+
+ +
+
+ +
+

7. Storage Element

+

Physical storage (database) with curved rectangle.

+
+ +
+
+ +
+

8. Conditional Gate

+

Diamond-shaped decision point with multiple output branches.

+
+ +
+
+ +
+

9. Complete Example

+

Comprehensive example showing all features together.

+
+ +
+
+ +
+

Syntax Reference

+
+# Modules
+module id "Label"           # Regular module
+library id "Label"          # Library module (underlined, reusable)
+storage id "Label"          # Physical storage (curved rectangle)
+
+# Connections
+from -> to                  # Simple connection
+from -> to data "label"     # Data flow (empty circle)
+from -> to control          # Control flow (filled circle)
+from -> to conditional      # Conditional call (diamond)
+
+# Loops
+loop over module1 module2 module3    # Curved arrow over modules
+
+# Layout
+# - Indentation (2 spaces per level) determines hierarchy
+# - Modules at same indentation appear on same level
+        
+
+ + +""" + + output_path = project_root / "test_output" / "index.html" + output_path.write_text(html, encoding='utf-8') + print(f"āœ… Created viewer: {output_path}") + print(f"\n🌐 Open in browser: file:///{output_path}") + + +def main(): + """Run all tests.""" + print("=" * 60) + print("Structure Chart Generator Tests") + print("=" * 60) + print() + + tests = [ + ("Basic Structure Chart", test_basic), + ("Connection Types", test_connection_types), + ("Library Module", test_library), + ("Loop Indicator", test_loop), + ("Partial Loop (2 of 4 modules)", test_partial_loop), + ("Multiple Arrows (same connection)", test_multiple_arrows), + ("Storage Element", test_storage), + ("Conditional Gate", test_conditional_gate), + ("Complete Example", test_complete), + ] + + for name, test_func in tests: + print(f"\nšŸ“Š Testing: {name}") + try: + svg = test_func() + # Basic validation + assert '' in svg, "SVG not closed" + assert 'class="structure-chart"' in svg, "Missing structure-chart class" + print(f" āœ… SVG validation passed") + except Exception as e: + print(f" āŒ Error: {e}") + import traceback + traceback.print_exc() + + print("\n" + "=" * 60) + print("Creating HTML viewer...") + print("=" * 60) + create_html_viewer() + + print("\n" + "=" * 60) + print("āœ… All tests complete!") + print("=" * 60) + + +if __name__ == "__main__": + main() From 48c6c42000a7002bc87dbb9a47e157a6436e8074 Mon Sep 17 00:00:00 2001 From: Eatham532 <78714349+Eatham532@users.noreply.github.com> Date: Tue, 28 Oct 2025 16:23:35 +1100 Subject: [PATCH 2/3] feat: integrate structure charts with 2.4 Signed-off-by: Eatham532 <78714349+Eatham532@users.noreply.github.com> --- .../index.md | 489 ++++++++---------- extensions/structure_chart.py | 10 +- 2 files changed, 228 insertions(+), 271 deletions(-) diff --git a/docs/Year11/ProgrammingFundamentals/Chapter-02-Designing-Algorithms/02-04-Structure-Charts-Abstraction-and-Refinement/index.md b/docs/Year11/ProgrammingFundamentals/Chapter-02-Designing-Algorithms/02-04-Structure-Charts-Abstraction-and-Refinement/index.md index 0dca71bd..b0e329a8 100644 --- a/docs/Year11/ProgrammingFundamentals/Chapter-02-Designing-Algorithms/02-04-Structure-Charts-Abstraction-and-Refinement/index.md +++ b/docs/Year11/ProgrammingFundamentals/Chapter-02-Designing-Algorithms/02-04-Structure-Charts-Abstraction-and-Refinement/index.md @@ -224,74 +224,75 @@ Most real projects use **both approaches**: ### Basic Structure Chart Elements -| Symbol | Meaning | Example | -|--------|---------|---------| -| Rectangle | Module/Function | `Calculate Grade` | -| Arrow | Calls/Uses | Module A → Module B | -| Circle on line | Data flow | `student_data` | -| Diamond | Selection | Choose based on condition | -| Curved arrow | Loop/Iteration | Repeat for each student | +**Structure charts** use a simple text-based syntax to represent program modules and their relationships: + +**Syntax Format:** +``` +module id "Module Name" + module child_id "Child Module Name" + +parent_id -> child_id [type] [label] +``` + +| Element | Example | Meaning | +|---------|---------|---------| +| Module | `module calc "Calculator"` | Define a function or component | +| Calls | `main -> calc` | Parent calls/uses Child | +| Data flow | `main -> calc data "numbers"` | Data passed between modules | +| Control flow | `main -> calc control` | Control information passed | +| Loop | `loop over child1 child2` | Repeat for multiple items | +| Library | `library util "Utilities"` | Reusable library module | +| Storage | `storage db "Database"` | Data storage element | /// details | Structure Chart Example: Grade Management System type: info open: false -```kroki-plantuml -@startuml -package "Grade Management System" { - [Main Program] as main - - package "Data Input" { - [Get Student Info] as getStudent - [Get Assignment Scores] as getScores - [Validate Input] as validate - } - - package "Calculations" { - [Calculate Average] as calcAvg - [Determine Letter Grade] as calcGrade - [Calculate GPA] as calcGPA - } - - package "Output" { - [Display Results] as display - [Generate Report] as report - [Save to File] as save - } -} - -main --> getStudent -main --> getScores -main --> calcAvg -main --> calcGrade -main --> display - -getStudent --> validate -getScores --> validate -calcAvg --> calcGrade -display --> report -report --> save - -note right of main : Controls overall flow -note right of validate : Ensures data quality -note right of calcGrade : Uses average to determine letter -@enduml - +```structure-chart +module main "Main Program" + module getStudent "Get Student Info" + module getScores "Get Assignment Scores" + module calcAvg "Calculate Average" + module calcGrade "Determine Letter Grade" + module display "Display Results" + module validate "Validate Input" + module report "Generate Report" + module save "Save to File" + +main -> getStudent +main -> getScores +main -> calcAvg +main -> calcGrade +main -> display + +getStudent -> validate +getScores -> validate + +calcAvg -> calcGrade +display -> report +report -> save ``` /// ### Data Flow in Structure Charts +```structure-chart +module main "Main Program" + module getStudent "Get Student Info" + module getScores "Get Assignment Scores" + module calcAvg "Calculate Average" + module calcGrade "Determine Letter Grade" + module display "Display Results" + +main -> getStudent data "student_name, student_id" +main -> getScores data "scores_list" +main -> calcAvg data "scores_list" +calcAvg -> calcGrade data "average_score" +main -> display data "student_name, average_score, letter_grade" ``` -Main Program -ā”œā”€ā”€ Get Student Info → student_name, student_id -ā”œā”€ā”€ Get Assignment Scores → scores_list -ā”œā”€ā”€ Calculate Average(scores_list) → average_score -ā”œā”€ā”€ Determine Letter Grade(average_score) → letter_grade -└── Display Results(student_name, average_score, letter_grade) -``` +Data flows from one module to another, transforming along the way. ## Stepwise Refinement @@ -380,74 +381,54 @@ END ### Structure Chart for Refined Solution -```kroki-plantuml -@startuml -package "Library Management System" { - [Main Controller] as main - - package "User Interface" { - [Display Menu] as menu - [Get User Input] as input - [Display Messages] as display - } - - package "Borrowing System" { - [Process Borrowing] as borrow - [Validate Card] as validateCard - [Check Book Availability] as checkBook - [Check Borrowing Limits] as checkLimits - [Create Borrowing Record] as createRecord - } - - package "Return System" { - [Process Return] as return - [Calculate Fines] as calcFines - [Update Records] as updateRec - } - - package "Database Operations" { - [Read Card Data] as readCard - [Read Book Data] as readBook - [Write Borrowing Data] as writeBorrow - [Write Return Data] as writeReturn - } - - package "Reporting" { - [Generate Reports] as reports - [Overdue Books Report] as overdue - [Popular Books Report] as popular - } -} - -main --> menu -main --> input -main --> borrow -main --> return -main --> reports - -borrow --> validateCard -borrow --> checkBook -borrow --> checkLimits -borrow --> createRecord -borrow --> display - -validateCard --> readCard -checkBook --> readBook -createRecord --> writeBorrow - -return --> calcFines -return --> updateRec -return --> display - -updateRec --> writeReturn - -reports --> overdue -reports --> popular -overdue --> readBook -overdue --> readCard -popular --> readBook -@enduml - +```structure-chart +module main "Main Controller" + module menu "Display Menu" + module input "Get User Input" + module borrow "Process Borrowing" + module validateCard "Validate Card" + module readCard "Read Card Data" + module checkBook "Check Book Availability" + module readBook "Read Book Data" + module checkLimits "Check Borrowing Limits" + module createRecord "Create Borrowing Record" + module writeBorrow "Write Borrowing Data" + module display "Display Messages" + module return "Process Return" + module calcFines "Calculate Fines" + module updateRec "Update Records" + module writeReturn "Write Return Data" + module reports "Generate Reports" + module overdue "Overdue Books Report" + module popular "Popular Books Report" + +main -> menu +main -> input +main -> borrow +main -> return +main -> reports + +borrow -> validateCard +borrow -> checkBook +borrow -> checkLimits +borrow -> createRecord +borrow -> display + +validateCard -> readCard +checkBook -> readBook +createRecord -> writeBorrow + +return -> calcFines +return -> updateRec +return -> display + +updateRec -> writeReturn + +reports -> overdue +reports -> popular +overdue -> readBook +overdue -> readCard +popular -> readBook ``` ## Benefits of Structured Design @@ -504,187 +485,161 @@ Using the stepwise refinement process: **Step 1: High-level modules (complete this)** -```kroki-plantuml -@startuml -package "Student Report System" { - [Main Program] - [Data Management] - [Grade Calculations] - [Report Generation] -} -@enduml +```structure-chart +module main "Main Program" + module dataMgmt "Data Management" + module gradeCalc "Grade Calculations" + module reportGen "Report Generation" +main -> dataMgmt +main -> gradeCalc +main -> reportGen ``` **Step 2: Refine each major module** -```kroki-plantuml -@startuml -package "Student Report System" { - [Main Program] as main - - package "Data Management" { - [_______________] as read - [_______________] as validate - [_______________] as store - } - - package "Grade Calculations" { - [_______________] as average - [_______________] as letterGrade - [_______________] as gpa - [_______________] as classStats - } - - package "Report Generation" { - [_______________] as individual - [_______________] as classReport - [_______________] as summary - [_______________] as export - } -} - -main --> read -main --> average -main --> individual - -read --> validate -validate --> store -average --> letterGrade -letterGrade --> gpa -individual --> export -classReport --> classStats -@enduml - +```structure-chart +module main "Main Program" + module read "Read Student Files" + module validate "Validate Data" + module store "Store in Memory" + module calcAvg "Calculate Averages" + module assignGrade "Assign Letter Grades" + module calcGPA "Calculate GPA" + module individualRpt "Individual Reports" + module export "Export to File" + module classReport "Class Summary Report" + module classStats "Class Statistics" + module gradeDist "Grade Distribution" + +main -> read +main -> calcAvg +main -> individualRpt + +read -> validate +validate -> store + +calcAvg -> assignGrade +assignGrade -> calcGPA + +individualRpt -> export +classReport -> classStats +gradeDist -> classStats ``` ### Solution **Step 2: Refined modules** -```kroki-plantuml -@startuml -package "Student Report System" { - [Main Program] as main - - package "Data Management" { - [Read Student Files] as read - [Validate Data] as validate - [Store in Memory] as store - } - - package "Grade Calculations" { - [Calculate Averages] as average - [Assign Letter Grades] as letterGrade - [Calculate GPA] as gpa - [Class Statistics] as classStats - } - - package "Report Generation" { - [Individual Reports] as individual - [Class Summary Report] as classReport - [Grade Distribution] as summary - [Export to File] as export - } -} - -main --> read -main --> average -main --> individual - -read --> validate -validate --> store -average --> letterGrade -letterGrade --> gpa -individual --> export -classReport --> classStats -summary --> classStats -@enduml - +```structure-chart +module main "Main Program" + module read "Read Student Files" + module validate "Validate Data" + module store "Store in Memory" + module calcAvg "Calculate Averages" + module assignGrade "Assign Letter Grades" + module calcGPA "Calculate GPA" + module individualRpt "Individual Reports" + module export "Export to File" + module classReport "Class Summary Report" + module classStats "Class Statistics" + module gradeDist "Grade Distribution" + +main -> read +main -> calcAvg +main -> individualRpt + +read -> validate +validate -> store + +calcAvg -> assignGrade +assignGrade -> calcGPA + +individualRpt -> export +classReport -> classStats +gradeDist -> classStats ``` **Step 3: Add data flow** -``` -Main Program -ā”œā”€ā”€ Read Student Files → student_records_list -ā”œā”€ā”€ Calculate Averages(student_records_list) → averages_list -ā”œā”€ā”€ Assign Letter Grades(averages_list) → grades_list -ā”œā”€ā”€ Calculate GPA(grades_list) → gpa_values -ā”œā”€ā”€ Individual Reports(student_records, grades, gpa) → report_files -└── Export to File(report_files) → saved_reports - +```structure-chart +module main "Main Program" + module read "Read Student Files" + module calcAvg "Calculate Averages" + module assignGrade "Assign Letter Grades" + module calcGPA "Calculate GPA" + module individualRpt "Individual Reports" + module export "Export to File" + +main -> read data "student_records_list" +main -> calcAvg data "student_records_list" +calcAvg -> assignGrade data "averages_list" +assignGrade -> calcGPA data "grades_list" +main -> individualRpt data "student_records, grades, gpa" +individualRpt -> export data "report_files" ``` ## Common Structure Chart Patterns ### Sequential Processing -```kroki-plantuml -@startuml -[Step 1] --> [Step 2] -[Step 2] --> [Step 3] -[Step 3] --> [Step 4] -@enduml +```structure-chart +module step1 "Step 1" + module step2 "Step 2" + module step3 "Step 3" + module step4 "Step 4" +step1 -> step2 +step2 -> step3 +step3 -> step4 ``` ### Data Transformation Pipeline -```kroki-plantuml -@startuml -[Input Data] --> [Validate] -[Validate] --> [Transform] -[Transform] --> [Calculate] -[Calculate] --> [Output] -@enduml - +```structure-chart +module input "Input Data" + module validate "Validate" + module transform "Transform" + module calculate "Calculate" + module output "Output" + +input -> validate +validate -> transform +transform -> calculate +calculate -> output ``` ### Service-Oriented Architecture -```kroki-plantuml -@startuml -package "Core Services" { - [Authentication Service] - [Data Service] - [Calculation Service] - [Reporting Service] -} - -[Main Application] --> [Authentication Service] -[Main Application] --> [Data Service] -[Main Application] --> [Calculation Service] -[Main Application] --> [Reporting Service] -@enduml - +```structure-chart +module app "Main Application" + module auth "Authentication Service" + module data "Data Service" + module calc "Calculation Service" + module report "Reporting Service" + +app -> auth +app -> data +app -> calc +app -> report ``` ### Layered Architecture -```kroki-plantuml -@startuml -package "Presentation Layer" { - [User Interface] - [Input Validation] -} - -package "Business Logic Layer" { - [Core Algorithms] - [Business Rules] -} +```structure-chart +module ui "User Interface" + module coreAlg "Core Algorithms" + module dbOps "Database Operations" -package "Data Access Layer" { - [Database Operations] - [File Operations] -} +module inputVal "Input Validation" + module bizRules "Business Rules" + module fileOps "File Operations" -[User Interface] --> [Core Algorithms] -[Core Algorithms] --> [Database Operations] -[Input Validation] --> [Business Rules] -[Business Rules] --> [File Operations] -@enduml +ui -> coreAlg +coreAlg -> dbOps +inputVal -> bizRules +bizRules -> fileOps ``` ## Design Guidelines diff --git a/extensions/structure_chart.py b/extensions/structure_chart.py index 3c3dae08..9035fa17 100644 --- a/extensions/structure_chart.py +++ b/extensions/structure_chart.py @@ -697,10 +697,12 @@ def run(self, lines): # Generate a unique ID for the diagram diagram_id = hashlib.md5(chart_def.encode()).hexdigest()[:8] - # Wrap in diagram container for modal support - new_lines.append(f'
') - new_lines.append(svg) - new_lines.append(f'') + # Wrap in diagram container for modal support (match kroki_wrapper format exactly) + new_lines.append(f'
') + new_lines.append(f'') + new_lines.append(f'
') + new_lines.append(f'

{svg}

') + new_lines.append('
') new_lines.append('
') new_lines.append('') From 499400a3f9394dd195918a049b254551697a3b52 Mon Sep 17 00:00:00 2001 From: Eatham532 <78714349+Eatham532@users.noreply.github.com> Date: Tue, 28 Oct 2025 16:24:32 +1100 Subject: [PATCH 3/3] Remove structure chart custom fence from mkdocs.yml Removed structure chart custom fence from mkdocs configuration. --- mkdocs.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 96310aa3..ea4452f5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -108,10 +108,6 @@ markdown_extensions: - pymdownx.tasklist - pymdownx.superfences: custom_fences: - # Structure Charts - - name: structure-chart - class: structure-chart - format: !!python/name:extensions.structure_chart.format_structure_chart # Python - name: python-exec class: python-exec