|
| 1 | +#!/bin/bash |
| 2 | + |
| 3 | +# Script to convert Hugo tabs shortcodes to tabpane format in markdown files |
| 4 | +# Usage: ./convert-tabs-to-tabpanes.sh <directory> |
| 5 | + |
| 6 | +# Removed set -e to prevent immediate exit on errors |
| 7 | + |
| 8 | +if [ $# -eq 0 ]; then |
| 9 | + echo "Usage: $0 <directory>" |
| 10 | + echo "Example: $0 /path/to/markdown/files" |
| 11 | + exit 1 |
| 12 | +fi |
| 13 | + |
| 14 | +DIRECTORY="$1" |
| 15 | + |
| 16 | +if [ ! -d "$DIRECTORY" ]; then |
| 17 | + echo "Error: Directory '$DIRECTORY' not found." |
| 18 | + exit 1 |
| 19 | +fi |
| 20 | + |
| 21 | +# Counter for processed files |
| 22 | +processed_files=0 |
| 23 | +files_with_changes=0 |
| 24 | + |
| 25 | +# Function to safely get numeric count from grep |
| 26 | +safe_count() { |
| 27 | + local pattern="$1" |
| 28 | + local file="$2" |
| 29 | + local result |
| 30 | + |
| 31 | + # Check if pattern contains regex characters that need -E flag |
| 32 | + if [[ "$pattern" =~ .*[\.\*\+\?\[\]\{\}\(\)\|].*$ ]]; then |
| 33 | + result=$(grep -E -c "$pattern" "$file" 2>/dev/null || echo "0") |
| 34 | + else |
| 35 | + result=$(grep -c "$pattern" "$file" 2>/dev/null || echo "0") |
| 36 | + fi |
| 37 | + |
| 38 | + # Ensure we have a valid number |
| 39 | + if [[ "$result" =~ ^[0-9]+$ ]]; then |
| 40 | + echo "$result" |
| 41 | + else |
| 42 | + echo "0" |
| 43 | + fi |
| 44 | +} |
| 45 | + |
| 46 | +# Function to process a single markdown file |
| 47 | +process_file() { |
| 48 | + local file="$1" |
| 49 | + echo "Processing: $file" |
| 50 | + |
| 51 | + # Create a backup |
| 52 | + cp "$file" "${file}.backup" |
| 53 | + |
| 54 | + # Use a temporary file for processing |
| 55 | + local temp_file=$(mktemp) |
| 56 | + cp "$file" "$temp_file" |
| 57 | + |
| 58 | + # Track if any changes were made |
| 59 | + local changes_made=false |
| 60 | + |
| 61 | + # Step 1: Replace [codetabs] with [tabpane] |
| 62 | + if grep -q '\[codetabs\]' "$temp_file"; then |
| 63 | + sed -i 's/\[codetabs\]/[tabpane]/g' "$temp_file" |
| 64 | + changes_made=true |
| 65 | + echo " - Replaced [codetabs] with [tabpane]" |
| 66 | + fi |
| 67 | + |
| 68 | + # Step 2: Change {{< ref filename >}} to {{% ref filename %}} |
| 69 | + # Handle both formats: with and without space before >}} |
| 70 | + # Use non-greedy matching to handle multiple links on the same line |
| 71 | + if grep -q '{{< ref [^}]*>}}' "$temp_file"; then |
| 72 | + sed -i 's/{{< ref \([^}]*\) >}}/{{% ref \1 %}}/g; s/{{< ref \([^}]*\)>}}/{{% ref \1 %}}/g' "$temp_file" |
| 73 | + changes_made=true |
| 74 | + echo " - Updated ref links from {{< >}} to {{% %}}" |
| 75 | + fi |
| 76 | + |
| 77 | + # Step 3: Check if file has codetab or tabs elements to determine if further processing is needed |
| 78 | + local has_codetabs=$(safe_count '{{% codetab' "$temp_file") |
| 79 | + local has_tabs_elements=$(safe_count '{{< tabs.*>}}' "$temp_file") |
| 80 | + |
| 81 | + if [ "$has_codetabs" -eq 0 ] && [ "$has_tabs_elements" -eq 0 ]; then |
| 82 | + echo " - No codetab or tabs elements found, skipping tab processing" |
| 83 | + else |
| 84 | + echo " - Found tab elements, processing..." |
| 85 | + local tabs_processed=false |
| 86 | + |
| 87 | + # Step 4: Replace opening {{% codetab %}} with {{% tab %}} |
| 88 | + if [ "$has_codetabs" -gt 0 ]; then |
| 89 | + if grep -q '{{% codetab' "$temp_file"; then |
| 90 | + sed -i 's/{{% codetab/{{% tab/g' "$temp_file" |
| 91 | + changes_made=true |
| 92 | + tabs_processed=true |
| 93 | + echo " - Replaced {{% codetab with {{% tab" |
| 94 | + fi |
| 95 | + |
| 96 | + # Step 5: Replace closing {{% /codetab %}} with {{% /tab %}} |
| 97 | + if grep -q '{{% /codetab' "$temp_file"; then |
| 98 | + sed -i 's/{{% \/codetab/{{% \/tab/g' "$temp_file" |
| 99 | + changes_made=true |
| 100 | + echo " - Replaced {{% /codetab with {{% /tab" |
| 101 | + fi |
| 102 | + fi |
| 103 | + |
| 104 | + # Step 6 & 7: Process tabs with languages using AWK |
| 105 | + if [ "$has_tabs_elements" -gt 0 ]; then |
| 106 | + awk ' |
| 107 | + BEGIN { |
| 108 | + in_tabs = 0 |
| 109 | + tab_index = 0 |
| 110 | + languages_count = 0 |
| 111 | + file_has_changes = 0 |
| 112 | + } |
| 113 | + |
| 114 | + # Step 6: Find {{< tabs LANGUAGES >}} and extract languages |
| 115 | + /^{{< tabs.*>}}/ { |
| 116 | + in_tabs = 1 |
| 117 | + tab_index = 0 |
| 118 | + |
| 119 | + # Extract everything after "tabs" and before ">}}" |
| 120 | + line = $0 |
| 121 | + # Remove the opening {{< tabs part (handle optional space after tabs) |
| 122 | + # This handles cases like "{{< tabs SDK HTTP>}}" and "{{< tabs SDK HTTP >}}" |
| 123 | + gsub(/^{{< tabs */, "", line) |
| 124 | + gsub(/>}}$/, "", line) |
| 125 | + gsub(/^ +/, "", line) # Remove leading spaces |
| 126 | + gsub(/ +$/, "", line) # Remove trailing spaces |
| 127 | + |
| 128 | + # Parse quoted and unquoted strings with improved logic |
| 129 | + languages_count = 0 |
| 130 | + i = 1 |
| 131 | + while (i <= length(line)) { |
| 132 | + # Skip whitespace |
| 133 | + while (i <= length(line) && substr(line, i, 1) == " ") { |
| 134 | + i++ |
| 135 | + } |
| 136 | + |
| 137 | + if (i > length(line)) break |
| 138 | + |
| 139 | + # Check if this token starts with a quote |
| 140 | + if (substr(line, i, 1) == "\"") { |
| 141 | + # Find the closing quote - handle quoted strings with spaces |
| 142 | + i++ # Skip opening quote |
| 143 | + start = i |
| 144 | + while (i <= length(line) && substr(line, i, 1) != "\"") { |
| 145 | + i++ |
| 146 | + } |
| 147 | + if (i <= length(line)) { |
| 148 | + # Found closing quote |
| 149 | + token = substr(line, start, i - start) |
| 150 | + if (token != "") { |
| 151 | + languages[++languages_count] = token |
| 152 | + } |
| 153 | + i++ # Skip closing quote |
| 154 | + } |
| 155 | + } else { |
| 156 | + # Unquoted token - read until space or end |
| 157 | + # This handles single words and dash-separated words |
| 158 | + start = i |
| 159 | + while (i <= length(line) && substr(line, i, 1) != " ") { |
| 160 | + i++ |
| 161 | + } |
| 162 | + token = substr(line, start, i - start) |
| 163 | + if (token != "") { |
| 164 | + languages[++languages_count] = token |
| 165 | + } |
| 166 | + } |
| 167 | + } |
| 168 | + |
| 169 | + print "{{< tabpane text=true >}}" |
| 170 | + file_has_changes = 1 |
| 171 | + next |
| 172 | + } |
| 173 | + |
| 174 | + # Step 7: Replace {{< /tabs >}} with {{< /tabpane >}} |
| 175 | + /^{{< \/tabs *>}}$/ { |
| 176 | + in_tabs = 0 |
| 177 | + print "{{< /tabpane >}}" |
| 178 | + file_has_changes = 1 |
| 179 | + next |
| 180 | + } |
| 181 | + |
| 182 | + # Update {{% tab %}} within tabs to include header parameter |
| 183 | + /^{{% tab *%}}$/ && in_tabs { |
| 184 | + if (tab_index < languages_count) { |
| 185 | + tab_index++ |
| 186 | + print "{{% tab header=\"" languages[tab_index] "\" %}}" |
| 187 | + file_has_changes = 1 |
| 188 | + } else { |
| 189 | + print $0 |
| 190 | + } |
| 191 | + next |
| 192 | + } |
| 193 | + |
| 194 | + # Print all other lines as-is |
| 195 | + { |
| 196 | + print $0 |
| 197 | + } |
| 198 | + |
| 199 | + END { |
| 200 | + # Return 0 if changes were made, 1 if no changes |
| 201 | + exit(file_has_changes ? 0 : 1) |
| 202 | + } |
| 203 | + ' "$temp_file" > "${temp_file}.awk" |
| 204 | + |
| 205 | + # Check if AWK made changes and copy result back to temp file |
| 206 | + if [ $? -eq 0 ]; then |
| 207 | + mv "${temp_file}.awk" "$temp_file" |
| 208 | + changes_made=true |
| 209 | + tabs_processed=true |
| 210 | + echo " - Processed tabs/tabpane conversions" |
| 211 | + else |
| 212 | + rm -f "${temp_file}.awk" |
| 213 | + fi |
| 214 | + fi |
| 215 | + |
| 216 | + # Step 8: Final validation check (only if tabs were processed) |
| 217 | + if [ "$tabs_processed" = true ]; then |
| 218 | + echo " - Running validation checks..." |
| 219 | + |
| 220 | + # Get counts safely from the processed temp file |
| 221 | + local opening_tabs=$(safe_count '{{% tab' "$temp_file") |
| 222 | + local closing_tabs=$(safe_count '{{% /tab' "$temp_file") |
| 223 | + local opening_tabpanes=$(safe_count '{{< tabpane' "$temp_file") |
| 224 | + local closing_tabpanes=$(safe_count '{{< /tabpane' "$temp_file") |
| 225 | + |
| 226 | + # Check for mismatches only if elements exist |
| 227 | + local validation_issues=false |
| 228 | + |
| 229 | + if [ "$opening_tabs" -gt 0 ] || [ "$closing_tabs" -gt 0 ]; then |
| 230 | + if [ "$opening_tabs" -ne "$closing_tabs" ]; then |
| 231 | + echo " ⚠️ WARNING: Unmatched tab elements - Opening: $opening_tabs, Closing: $closing_tabs" |
| 232 | + validation_issues=true |
| 233 | + fi |
| 234 | + fi |
| 235 | + |
| 236 | + if [ "$opening_tabpanes" -gt 0 ] || [ "$closing_tabpanes" -gt 0 ]; then |
| 237 | + if [ "$opening_tabpanes" -ne "$closing_tabpanes" ]; then |
| 238 | + echo " ⚠️ WARNING: Unmatched tabpane elements - Opening: $opening_tabpanes, Closing: $closing_tabpanes" |
| 239 | + validation_issues=true |
| 240 | + fi |
| 241 | + fi |
| 242 | + |
| 243 | + if [ "$validation_issues" = false ]; then |
| 244 | + echo " ✅ Validation passed: All tab and tabpane elements are properly matched" |
| 245 | + fi |
| 246 | + fi |
| 247 | + fi |
| 248 | + |
| 249 | + # Copy temp file back to original if changes were made |
| 250 | + if [ "$changes_made" = true ]; then |
| 251 | + cp "$temp_file" "$file" |
| 252 | + fi |
| 253 | + |
| 254 | + # Clean up temp file |
| 255 | + rm "$temp_file" |
| 256 | + |
| 257 | + if [ "$changes_made" = true ]; then |
| 258 | + echo " ✅ File processed with changes" |
| 259 | + ((files_with_changes++)) |
| 260 | + else |
| 261 | + echo " ℹ️ No changes needed" |
| 262 | + # Remove backup if no changes were made |
| 263 | + rm "${file}.backup" |
| 264 | + fi |
| 265 | + |
| 266 | + ((processed_files++)) |
| 267 | + echo "" |
| 268 | +} |
| 269 | + |
| 270 | +echo "Starting conversion of tabs to tabpanes in directory: $DIRECTORY" |
| 271 | +echo "=============================================================" |
| 272 | +echo "" |
| 273 | + |
| 274 | +# Debug: Show what files will be processed |
| 275 | +echo "Discovering markdown files..." |
| 276 | +file_count=0 |
| 277 | +while IFS= read -r -d '' file; do |
| 278 | + echo "Found: $file" |
| 279 | + ((file_count++)) |
| 280 | +done < <(find "$DIRECTORY" -name "*.md" -type f -print0) |
| 281 | +echo "Total files found: $file_count" |
| 282 | +echo "" |
| 283 | + |
| 284 | +# Find all markdown files recursively and process them |
| 285 | +while IFS= read -r -d '' file; do |
| 286 | + process_file "$file" |
| 287 | +done < <(find "$DIRECTORY" -name "*.md" -type f -print0) |
| 288 | + |
| 289 | +echo "=============================================================" |
| 290 | +echo "Conversion completed!" |
| 291 | +echo "Files processed: $processed_files" |
| 292 | +echo "Files with changes: $files_with_changes" |
| 293 | +echo "" |
| 294 | + |
| 295 | +if [ $files_with_changes -gt 0 ]; then |
| 296 | + echo "Backup files created with .backup extension for files that were modified." |
| 297 | + echo "You can remove them with: find $DIRECTORY -name '*.backup' -delete" |
| 298 | +fi |
| 299 | + |
| 300 | +echo "" |
| 301 | +echo "Summary of transformations applied:" |
| 302 | +echo "1. ✅ [codetabs] → [tabpane]" |
| 303 | +echo "2. ✅ {{< ref filename>}} → {{% ref filename %}}" |
| 304 | +echo "3. ✅ Conditional processing based on presence of codetab/tabs elements" |
| 305 | +echo "4. ✅ {{% codetab %}} → {{% tab %}} (if codetabs found)" |
| 306 | +echo "5. ✅ {{% /codetab %}} → {{% /tab %}} (if codetabs found)" |
| 307 | +echo "6. ✅ {{< tabs LANGUAGES >}} → {{< tabpane text=true >}} (if tabs found)" |
| 308 | +echo "7. ✅ {{< /tabs >}} → {{< /tabpane >}} (if tabs found)" |
| 309 | +echo "8. ✅ Validation check (only for files with processed tabs)" |
| 310 | + |
| 311 | +exit 0 |
0 commit comments