Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
358 changes: 350 additions & 8 deletions lib/dispatchers/teach-dispatcher.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -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+)
# ==================================================================
Expand Down Expand Up @@ -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 <input_file> <output_file> [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"
Expand Down Expand Up @@ -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"
Expand Down