## How to generate the table - Make sure [`uv`](https://docs.astral.sh/uv/getting-started/installation/) is installed in your machine - Download the python script below and save it as `hackmd_table_generator.py` - Make it executable (`chmod a+x hackmd_table_generator.py`) - Export an `HACKMD_API_TOKEN` environment variable with the HackMD API key (`export HACKMD_API_TOKEN=xxxxxxxxxxxxxxx`) - Run the script with the proper arguments. For example: ```bash! ./hackmd_table_generator.py --team gridtools --tags "cycle 30 07/25" --columns "Appetite,Developers,Support" --exclude 'OVERVIEW.*,.*[Bb]rainstorming.*' -o cycle30.md ``` ## `hackmd_table_generator.py` script ```python! #!/usr/bin/env -S uv run -q --script # /// script # requires-python = ">=3.10" # dependencies = [ # "requests>=2.25.0", # "typer>=0.9.0", # ] # /// import os import sys import requests import typer import re from typing import Any class HackMDAPI: def __init__(self, token: str): self.token = token self.base_url = "https://api.hackmd.io/v1" self.headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json" } def get_team_notes(self, team_path: str, tags: list[str]) -> list[dict[str, Any]]: """Get notes from a team that contain all specified tags.""" url = f"{self.base_url}/teams/{team_path}/notes" try: response = requests.get(url, headers=self.headers) response.raise_for_status() notes = response.json() # Filter notes by tags filtered_notes = [] for note in notes: note_tags = note.get('tags', []) if all(tag in note_tags for tag in tags): filtered_notes.append(note) return filtered_notes except requests.exceptions.RequestException as e: typer.echo(f"Error fetching notes: {e}", err=True) raise typer.Exit(1) def filter_notes_by_exclude_patterns(notes: list[dict[str, Any]], exclude_patterns: list[str]) -> list[dict[str, Any]]: """Filter out notes whose titles match any of the exclude patterns.""" if not exclude_patterns: return notes compiled_patterns = [] for pattern in exclude_patterns: try: compiled_patterns.append(re.compile(pattern, re.IGNORECASE)) except re.error as e: typer.echo(f"Error compiling regex pattern '{pattern}': {e}", err=True) raise typer.Exit(1) filtered_notes = [] for note in notes: title = note.get('title', 'Untitled') # Check if title matches any exclude pattern if not any(pattern.search(title) for pattern in compiled_patterns): filtered_notes.append(note) return filtered_notes def create_reference_name(title: str, used_names: set) -> str: """Create a meaningful reference name from title, max 15 chars, ensuring uniqueness.""" # Remove special characters and convert to lowercase clean_title = re.sub(r'[^a-zA-Z0-9\s-]', '', title.lower()) # Replace spaces and multiple hyphens with single hyphen clean_title = re.sub(r'[\s-]+', '-', clean_title) # Remove leading/trailing hyphens clean_title = clean_title.strip('-') # If empty after cleaning, use fallback if not clean_title: clean_title = "untitled" # Truncate to 15 chars base_name = clean_title[:15].rstrip('-') # Ensure uniqueness ref_name = base_name counter = 1 while ref_name in used_names: # Calculate space needed for counter suffix suffix = f"-{counter}" max_base_len = 15 - len(suffix) ref_name = base_name[:max_base_len].rstrip('-') + suffix counter += 1 used_names.add(ref_name) return ref_name def generate_markdown_table(notes: list[dict[str, Any]], additional_columns: list[str]) -> str: """Generate a markdown table from the notes data.""" if not notes: return "No documents found with the specified tags.\n" # Sort notes alphabetically by title sorted_notes = sorted(notes, key=lambda note: note.get('title', 'Untitled').lower()) # Define columns columns = ["Title"] + additional_columns # Create header header = "| " + " | ".join(columns) + " |" separator = "| " + " | ".join(["---"] * len(columns)) + " |" # Create rows and collect references rows = [] references = [] used_ref_names = set() for note in sorted_notes: title = note.get('title', 'Untitled') note_id = note.get('id', '') # Create reference-style link for title if note_id: ref_name = create_reference_name(title, used_ref_names) title_link = f"[{title}][{ref_name}]" references.append(f"[{ref_name}]: https://hackmd.io/{note_id}") else: title_link = title # Create row with title and empty cells for additional columns row_data = [title_link] + [""] * len(additional_columns) row = "| " + " | ".join(row_data) + " |" rows.append(row) # Combine all parts table_parts = [header, separator] + rows # Add references at the bottom if any exist if references: table_parts.extend(["", ""] + references) return "\n".join(table_parts) + "\n" def main( team: str = typer.Option(..., "--team", "-t", help="HackMD team path/name"), tags: str = typer.Option(..., "--tags", "-g", help="Comma-separated list of tags to filter by"), columns: str = typer.Option("", "--columns", "-c", help="Comma-separated list of additional column names"), exclude: str = typer.Option("", "--exclude", "-e", help="Comma-separated list of regex patterns to exclude notes by title"), output: str | None = typer.Option(None, "--output", "-o", help="Output file path (default: stdout)") ): """ Generate a markdown table of HackMD documents with specified tags. Requires HACKMD_API_TOKEN environment variable to be set. """ # Get API token from environment api_token = os.getenv('HACKMD_API_TOKEN') if not api_token: typer.echo("Error: HACKMD_API_TOKEN environment variable not set", err=True) raise typer.Exit(1) # Parse tags, columns, and exclude patterns tag_list = [tag.strip() for tag in tags.split(',') if tag.strip()] column_list = [col.strip() for col in columns.split(',') if col.strip()] if columns else [] exclude_patterns = [pattern.strip() for pattern in exclude.split(',') if pattern.strip()] if exclude else [] if not tag_list: typer.echo("Error: At least one tag must be specified", err=True) raise typer.Exit(1) # Initialize API client api = HackMDAPI(api_token) # Fetch notes typer.echo(f"Fetching notes from team '{team}' with tags: {', '.join(tag_list)}", err=True) notes = api.get_team_notes(team, tag_list) # Apply exclude patterns if exclude_patterns: typer.echo(f"Applying exclude patterns: {', '.join(exclude_patterns)}", err=True) notes = filter_notes_by_exclude_patterns(notes, exclude_patterns) # Generate table table = generate_markdown_table(notes, column_list) # Output table if output: try: with open(output, 'w', encoding='utf-8') as f: f.write(table) typer.echo(f"Table written to {output}", err=True) except IOError as e: typer.echo(f"Error writing to file: {e}", err=True) raise typer.Exit(1) else: typer.echo(table) typer.echo(f"Found {len(notes)} documents", err=True) if __name__ == "__main__": typer.run(main) ```