diff --git a/lib/dispatchers/teach-dispatcher.zsh b/lib/dispatchers/teach-dispatcher.zsh index 28d21384..494f9ade 100644 --- a/lib/dispatchers/teach-dispatcher.zsh +++ b/lib/dispatchers/teach-dispatcher.zsh @@ -2033,6 +2033,32 @@ _teach_scholar_wrapper() { fi fi + # Special case: slides --from-lecture (v5.15.0+) + # Converts lecture .qmd files to RevealJS slides + if [[ "$subcommand" == "slides" ]]; then + local from_lecture="" + local week_num="" + for ((i=1; i<=${#args[@]}; i++)); do + if [[ "${args[$i]}" == "--from-lecture" ]]; then + from_lecture="${args[$((i+1))]}" + elif [[ "${args[$i]}" =~ ^--from-lecture= ]]; then + from_lecture="${args[$i]#*=}" + elif [[ "${args[$i]}" == "--week" || "${args[$i]}" == "-w" ]]; then + week_num="${args[$((i+1))]}" + elif [[ "${args[$i]}" =~ ^--week= ]]; then + week_num="${args[$i]#*=}" + elif [[ "${args[$i]}" =~ ^-w= ]]; then + week_num="${args[$i]#*=}" + fi + done + + # If --from-lecture provided OR --week provided (auto-detect lecture files) + if [[ -n "$from_lecture" ]] || [[ -n "$week_num" ]]; then + _teach_slides_from_lecture "$from_lecture" "$week_num" "${args[@]}" + return $? + fi + fi + # ================================================================== # PHASE 5: Revision Workflow (v5.13.0+) # ================================================================== @@ -2238,6 +2264,312 @@ _teach_lecture_from_plan() { _teach_execute "$scholar_cmd" "true" } +# ============================================================================ +# SLIDES FROM LECTURE (v5.15.0+) +# Converts lecture .qmd files to RevealJS slides +# ============================================================================ + +# Generate slides from lecture .qmd files +# Usage: _teach_slides_from_lecture [lecture_file] [week_num] [extra_args...] +_teach_slides_from_lecture() { + local from_lecture="$1" + local week_num="$2" + shift 2 + local -a extra_args=("$@") + local config_file=".flow/teach-config.yml" + local -a lecture_files=() + local verbose=false + local dry_run=false + local output_dir="slides" + + # Parse extra args for verbose and dry-run + for arg in "${extra_args[@]}"; do + [[ "$arg" == "--verbose" || "$arg" == "-v" ]] && verbose=true + [[ "$arg" == "--dry-run" ]] && dry_run=true + done + + # ───────────────────────────────────────────────────────────────────── + # Step 1: Determine lecture files to convert + # ───────────────────────────────────────────────────────────────────── + + if [[ -n "$from_lecture" ]]; then + # Explicit file provided + if [[ -f "$from_lecture" ]]; then + lecture_files+=("$from_lecture") + else + _teach_error "Lecture file not found: $from_lecture" + return 1 + fi + elif [[ -n "$week_num" ]]; then + # Week number provided - look up files from teach-config.yml + if [[ ! -f "$config_file" ]]; then + _teach_error "No teach-config.yml found" "Run 'teach init' first" + return 1 + fi + + if ! command -v yq &>/dev/null; then + _teach_error "yq required for config parsing" "Install: brew install yq" + return 1 + fi + + # Check if week has parts structure + local has_parts + has_parts=$(yq ".semester_info.weeks[] | select(.number == $week_num) | .parts // null" "$config_file" 2>/dev/null) + + if [[ "$has_parts" != "null" && -n "$has_parts" ]]; then + # Multi-part week - get all part files + local -a part_files + part_files=($(yq ".semester_info.weeks[] | select(.number == $week_num) | .parts[].file" "$config_file" 2>/dev/null)) + for pf in "${part_files[@]}"; do + if [[ -f "$pf" ]]; then + lecture_files+=("$pf") + else + _teach_warn "Part file not found: $pf" + fi + done + else + # Single lecture week - try to find lecture file + local lecture_pattern="lectures/week-$(printf '%02d' $week_num)*.qmd" + for f in $~lecture_pattern; do + [[ -f "$f" ]] && lecture_files+=("$f") + done + fi + + if [[ ${#lecture_files[@]} -eq 0 ]]; then + _teach_error "No lecture files found for week $week_num" + return 1 + fi + else + _teach_error "Specify --from-lecture FILE or --week N" + return 1 + fi + + [[ "$verbose" == "true" ]] && echo "📄 Found ${#lecture_files[@]} lecture file(s) to convert" + + # ───────────────────────────────────────────────────────────────────── + # Step 2: Process each lecture file + # ───────────────────────────────────────────────────────────────────── + + local -a generated_files=() + + for lecture_file in "${lecture_files[@]}"; do + [[ "$verbose" == "true" ]] && echo "📖 Processing: $lecture_file" + + # Generate output filename + local basename="${lecture_file:t:r}" # Remove path and extension + local output_file="${output_dir}/${basename}_slides.qmd" + + # Create output directory if needed + [[ ! -d "$output_dir" ]] && mkdir -p "$output_dir" + + if [[ "$dry_run" == "true" ]]; then + echo "" + echo "📋 Dry-run: Would generate slides from $lecture_file" + echo " Output: $output_file" + _teach_lecture_to_slides_preview "$lecture_file" + else + # Generate the slides + _teach_convert_lecture_to_slides "$lecture_file" "$output_file" "$verbose" + local exit_code=$? + + if [[ $exit_code -eq 0 ]]; then + generated_files+=("$output_file") + echo "✅ Generated: $output_file" + else + _teach_warn "Failed to convert: $lecture_file" + fi + fi + done + + # ───────────────────────────────────────────────────────────────────── + # Step 3: Summary + # ───────────────────────────────────────────────────────────────────── + + if [[ "$dry_run" != "true" && ${#generated_files[@]} -gt 0 ]]; then + echo "" + echo "📊 Generated ${#generated_files[@]} slide file(s):" + for f in "${generated_files[@]}"; do + echo " • $f" + done + echo "" + echo "💡 Next steps:" + echo " 1. Review and customize the generated slides" + echo " 2. Run: quarto preview ${generated_files[1]}" + echo " 3. Add to _quarto.yml navigation if needed" + fi + + return 0 +} + +# Preview what would be extracted from lecture file (dry-run) +_teach_lecture_to_slides_preview() { + local lecture_file="$1" + + # Count sections, code chunks, callouts + local h2_count h3_count code_chunks callouts + + h2_count=$(grep -c "^## " "$lecture_file" 2>/dev/null || echo 0) + h3_count=$(grep -c "^### " "$lecture_file" 2>/dev/null || echo 0) + code_chunks=$(grep -c '```{r' "$lecture_file" 2>/dev/null || echo 0) + callouts=$(grep -c '::: {.callout' "$lecture_file" 2>/dev/null || echo 0) + + echo "" + echo " Content analysis:" + echo " ├── H2 sections (slides): $h2_count" + echo " ├── H3 subsections: $h3_count" + echo " ├── R code chunks: $code_chunks" + echo " └── Callout boxes: $callouts" + echo "" + echo " Estimated slides: ~$((h2_count + h3_count / 2))" +} + +# Convert a single lecture file to RevealJS slides +# Usage: _teach_convert_lecture_to_slides [verbose] +_teach_convert_lecture_to_slides() { + local input_file="$1" + local output_file="$2" + local verbose="${3:-false}" + + # Extract YAML frontmatter using yq for proper parsing + local title subtitle author date + title=$(yq '.title // ""' "$input_file" 2>/dev/null) + subtitle=$(yq '.subtitle // ""' "$input_file" 2>/dev/null) + author=$(yq '.author // ""' "$input_file" 2>/dev/null) + date=$(yq '.date // ""' "$input_file" 2>/dev/null) + + # Generate RevealJS YAML header + { + echo "---" + echo "title: \"${title:-Lecture Slides}\"" + echo "subtitle: \"${subtitle:-}\"" + echo "author: \"${author:-}\"" + echo "date: \"${date:-}\"" + echo "format:" + echo " revealjs:" + echo " theme: [default, custom.scss]" + echo " slide-number: true" + echo " chalkboard: true" + echo " code-line-numbers: true" + echo " code-overflow: wrap" + echo " highlight-style: github" + echo " footer: \"${title:-}\"" + echo "execute:" + echo " echo: true" + echo " warning: false" + echo "---" + echo "" + } > "$output_file" + + # Process the lecture content + # Skip the YAML frontmatter and process the rest + local in_frontmatter=false + local frontmatter_count=0 + local in_code_block=false + local in_callout=false + local callout_depth=0 + local current_section="" + local slide_count=0 + local line="" + + while IFS= read -r line || [[ -n "${line}" ]]; do + # Track frontmatter + if [[ "$line" == "---" ]]; then + ((frontmatter_count++)) + if [[ $frontmatter_count -le 2 ]]; then + continue # Skip YAML frontmatter + fi + fi + + # Skip until past frontmatter + [[ $frontmatter_count -lt 2 ]] && continue + + # Track code blocks (don't modify content inside) + if [[ "$line" =~ ^\`\`\` ]]; then + in_code_block=$([[ "$in_code_block" == "true" ]] && echo "false" || echo "true") + fi + + # Track callouts + if [[ "$line" =~ '^:::' && "$line" =~ '\{\.callout' ]]; then + in_callout=true + ((callout_depth++)) + elif [[ "$line" == ":::" && "$in_callout" == "true" ]]; then + ((callout_depth--)) + [[ $callout_depth -eq 0 ]] && in_callout=false + fi + + # Convert H1 to slide title (level 1 becomes title slide) + if [[ "$line" =~ ^#\ && ! "$line" =~ ^##\ ]]; then + # H1 becomes a section title slide + printf '\n' >> "$output_file" + printf '%s {.center}\n' "$line" >> "$output_file" + printf '\n' >> "$output_file" + ((slide_count++)) + continue + fi + + # H2 becomes new slide + if [[ "$line" =~ ^##\ && ! "$line" =~ ^###\ ]]; then + printf '\n' >> "$output_file" + printf '%s\n' "$line" >> "$output_file" + ((slide_count++)) + continue + fi + + # H3 with content becomes slide with incremental reveal + if [[ "$line" =~ ^###\ ]]; then + printf '\n' >> "$output_file" + printf '%s\n' "$line" >> "$output_file" + continue + fi + + # Convert TL;DR boxes to callout-note for slides + if [[ "$line" =~ ':::.+\{\.tldr-box\}' ]]; then + printf '::: {.callout-tip}\n' >> "$output_file" + printf '## Key Points\n' >> "$output_file" + continue + fi + + # Convert checkpoint questions to interactive elements + if [[ "$line" =~ "Checkpoint Question" ]]; then + printf '\n' >> "$output_file" + printf '::: {.callout-warning}\n' >> "$output_file" + printf '## 🤔 Checkpoint\n' >> "$output_file" + continue + fi + + # Pass through code chunks (important for R examples) + # Use printf '%s\n' to preserve LaTeX backslashes like \tau, \beta, \alpha + if [[ "$in_code_block" == "true" ]] || [[ "$line" =~ ^\`\`\` ]]; then + printf '%s\n' "$line" >> "$output_file" + continue + fi + + # Convert columns to slide-friendly format + if [[ "$line" =~ ':::.+\{\.columns\}' ]]; then + printf '\n' >> "$output_file" + printf ':::: {.columns}\n' >> "$output_file" + continue + fi + + if [[ "$line" =~ ':::.+\{\.column' ]]; then + printf '\n' >> "$output_file" + printf '%s\n' "$line" >> "$output_file" + continue + fi + + # Pass through most content + # IMPORTANT: Use printf '%s\n' instead of echo to preserve LaTeX backslashes + # echo interprets escape sequences like \t (tab), \b (backspace), \v (vertical tab) + # which corrupts LaTeX commands like \tau, \beta, \varepsilon, \underbrace, \alpha + printf '%s\n' "$line" >> "$output_file" + + done < "$input_file" + + [[ "$verbose" == "true" ]] && echo " Created $slide_count slides" + + return 0 +} + # Archive semester backups (v5.14.0 - Task 5) _teach_archive_command() { local config_file=".flow/teach-config.yml" @@ -2423,18 +2755,28 @@ _teach_scholar_help() { echo "teach slides - Generate presentation slides" echo "" echo "Usage: teach slides \"Topic\" [options]" + echo " teach slides --week N [options] # Convert lecture to slides" + echo " teach slides --from-lecture FILE # Convert specific file" _show_universal_flags echo "${FLOW_COLORS[info]}Slides-Specific Options:${FLOW_COLORS[reset]}" - echo " --theme NAME Slide theme (default, academic, minimal)" - echo " --from-lecture FILE Generate from lecture file" - echo " --format FORMAT Output format (quarto, markdown)" - echo " --dry-run Preview without saving" + echo " --theme NAME Slide theme (default, academic, minimal)" + echo " --from-lecture FILE Convert lecture .qmd to slides (preserves R code)" + echo " --week N, -w N Auto-detect lecture file(s) from config" + echo " --format FORMAT Output format (quarto, markdown)" + echo " --dry-run Preview content analysis without generating" + echo " --verbose, -v Show detailed progress" + echo "" + echo "${FLOW_COLORS[bold]}LECTURE CONVERSION (v5.15.0+)${FLOW_COLORS[reset]}" + echo " Converts existing lecture .qmd files to RevealJS slides." + echo " Preserves R code chunks, callouts, columns, and examples." + echo " Multi-part weeks (defined in teach-config.yml) generate separate slides." echo "" echo "${FLOW_COLORS[bold]}EXAMPLES${FLOW_COLORS[reset]}" - echo " teach slides \"Multiple Regression\" # Basic slides" - echo " teach slides \"Logistic Regression\" --week 10 # From lesson plan" - echo " teach slides \"GLMs\" --theme minimal # Minimal theme" - echo " teach slides \"Bayesian Stats\" -x -c # Examples + code" + echo " teach slides --week 1 # Convert Week 1 lecture(s)" + echo " teach slides --week 1 --dry-run # Preview what would be generated" + echo " teach slides --from-lecture lectures/week-01_intro.qmd # Specific file" + echo " teach slides \"Multiple Regression\" # Generate from topic (Scholar)" + echo " teach slides \"GLMs\" --theme minimal # With theme" ;; exam) echo "teach exam - Generate exam questions"