Skip to content

Instantly share code, notes, and snippets.

@huytd
Created February 23, 2026 18:25
Show Gist options
  • Select an option

  • Save huytd/8f4e218be27ff49b2e33f8fcc799e07b to your computer and use it in GitHub Desktop.

Select an option

Save huytd/8f4e218be27ff49b2e33f8fcc799e07b to your computer and use it in GitHub Desktop.
Markdown render using Bash script
#!/usr/bin/env bash
# Usage: ./mdview filename.md OR cat file.md | ./mdview
# --- Configuration ---
MAX_CELL_WIDTH=${MAX_CELL_WIDTH:-50}
# --- ANSI Colors ---
ESC=$'\033'
RESET="${ESC}[0m"
BOLD="${ESC}[1m"
ITALIC="${ESC}[3m"
CODE_STYLE="${ESC}[48;5;236m${ESC}[38;5;189m"
HEADER_1="${ESC}[1;38;5;39m"
HEADER_2="${ESC}[1;38;5;75m"
HEADER_3="${ESC}[1;38;5;111m"
BULLET_COLOR="${ESC}[38;5;250m"
in_code_block=0
table_buffer=()
# Helper: Get the length of a string ignoring ANSI escape codes
visible_len() {
local vis=$(printf '%s' "$1" | sed -E "s/${ESC}\\[[0-9;]*[a-zA-Z]//g")
echo "${#vis}"
}
# Render buffered table data with text wrapping
flush_table() {
if [[ ${#table_buffer[@]} -eq 0 ]]; then return; fi
local col_widths=()
local parsed_rows=()
local num_cols=0
local r c cell i pad row
# Pass 1: Parse rows and determine column widths
for row in "${table_buffer[@]}"; do
# Skip Markdown separator lines (e.g., |---|:---:|)
if [[ "$row" =~ ^[[:space:]]*\|?[[:space:]]*:?-+[-:|[:space:]]*$ ]]; then continue; fi
# Strip leading/trailing pipes
row=$(printf '%s\n' "$row" | sed -E 's/^[[:space:]]*\||\|[[:space:]]*$//g')
parsed_rows+=("$row")
IFS='|' read -r -a cells <<< "${row} "
c=0
for cell in "${cells[@]}"; do
cell=$(printf '%s\n' "$cell" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')
# Find the longest single word to ensure we don't chop URLs
local max_word_len=0
local IFS_space=' '
read -ra words <<< "$cell"
for w in "${words[@]}"; do
local wlen=$(visible_len "$w")
if (( wlen > max_word_len )); then max_word_len=$wlen; fi
done
local total_len=$(visible_len "$cell")
local target_width=$total_len
# Enforce the MAX_CELL_WIDTH limit, but respect unbroken long words
if (( target_width > MAX_CELL_WIDTH )); then target_width=$MAX_CELL_WIDTH; fi
if (( target_width < max_word_len )); then target_width=$max_word_len; fi
if [[ -z "${col_widths[$c]}" ]] || (( target_width > col_widths[$c] )); then
col_widths[$c]=$target_width
fi
((c++))
done
if (( c > num_cols )); then num_cols=$c; fi
done
# Helper: Draw horizontal table dividers
draw_sep() {
local left=$1 mid=$2 right=$3
printf "%s" "$left"
for ((c=0; c<num_cols; c++)); do
for ((i=0; i<col_widths[c]+2; i++)); do printf "─"; done
if (( c < num_cols - 1 )); then printf "%s" "$mid"; fi
done
printf "%s\n" "$right"
}
# Pass 2: Word Wrap & Draw the table
draw_sep "┌" "┬" "┐"
r=0
for row in "${parsed_rows[@]}"; do
IFS='|' read -r -a cells <<< "${row} "
local max_subrows=1
local -a wrapped_cols=()
# Wrap each cell into lines
for ((c=0; c<num_cols; c++)); do
local cell="${cells[$c]}"
cell=$(printf '%s\n' "$cell" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')
local width="${col_widths[$c]}"
local -a current_cell_lines=()
local current_line=""
local current_len=0
local IFS_space=' '
read -ra words <<< "$cell"
for w in "${words[@]}"; do
local wlen=$(visible_len "$w")
if (( current_len == 0 )); then
current_line="$w"
current_len=$wlen
elif (( current_len + 1 + wlen > width )); then
current_cell_lines+=("$current_line")
current_line="$w"
current_len=$wlen
else
current_line="$current_line $w"
current_len=$((current_len + 1 + wlen))
fi
done
if [[ -n "$current_line" ]]; then
current_cell_lines+=("$current_line")
fi
# Save the lines joined by a newline for easy extraction later
local joined=""
for ((i=0; i<${#current_cell_lines[@]}; i++)); do
if (( i > 0 )); then joined+=$'\n'; fi
joined+="${current_cell_lines[$i]}"
done
wrapped_cols[$c]="$joined"
if (( ${#current_cell_lines[@]} > max_subrows )); then
max_subrows=${#current_cell_lines[@]}
fi
done
# Print the wrapped sub-rows line-by-line
for ((sub=0; sub<max_subrows; sub++)); do
printf "│"
for ((c=0; c<num_cols; c++)); do
local cell_lines_str="${wrapped_cols[$c]}"
local line_val=""
# Extract the specific line string for this sub-row
local i=0
local old_IFS="$IFS"
IFS=$'\n'
for ln in $cell_lines_str; do
if (( i == sub )); then
line_val="$ln"
break
fi
((i++))
done
IFS="$old_IFS"
local vlen=$(visible_len "$line_val")
local pad=$(( col_widths[c] - vlen ))
if (( pad < 0 )); then pad=0; fi
# RESET is called before padding spaces so backgrounds don't bleed out of the text
printf " %s${RESET}" "$line_val"
for ((i=0; i<pad; i++)); do printf " "; done
printf " │"
done
printf "\n"
done
if (( r == 0 )); then
draw_sep "├" "┼" "┤"
fi
((r++))
done
draw_sep "└" "┴" "┘"
table_buffer=()
}
# --- Main Parsing Loop ---
while IFS= read -r line || [[ -n "$line" ]]; do
# Fenced code block toggle
if [[ "$line" =~ ^\`\`\` ]]; then
flush_table
in_code_block=$((1 - in_code_block))
continue
fi
# Inside code block
if (( in_code_block )); then
printf "${CODE_STYLE}%s${RESET}\n" "$line"
continue
fi
# Inline replacements (Bold, Italic, Code) via sed
formatted=$(printf '%s' "$line" | sed -E \
-e "s/\`([^\`]+)\`/${CODE_STYLE} \1 ${RESET}/g" \
-e "s/\*\*([^*]+)\*\*/${BOLD}\1${RESET}/g" \
-e "s/__([^_]+)__/${BOLD}\1${RESET}/g" \
-e "s/\*([^*]+)\*/${ITALIC}\1${RESET}/g" \
-e "s/_([^_]+)_/${ITALIC}\1${RESET}/g")
# Table detection (starts with optional spaces then a pipe)
if [[ "$formatted" =~ ^[[:space:]]*\| ]]; then
table_buffer+=("$formatted")
continue
else
flush_table
fi
# Block-level rendering
if [[ "$formatted" =~ ^'### ' ]]; then
printf "${HEADER_3}%s${RESET}\n" "${formatted#\#\#\# }"
elif [[ "$formatted" =~ ^'## ' ]]; then
printf "${HEADER_2}%s${RESET}\n" "${formatted#\#\# }"
elif [[ "$formatted" =~ ^'# ' ]]; then
printf "${HEADER_1}%s${RESET}\n" "${formatted#\# }"
elif [[ "$formatted" =~ ^'- ' ]]; then
printf "${BULLET_COLOR}•${RESET} %s\n" "${formatted#- }"
else
printf "%s\n" "$formatted"
fi
done < "${1:-/dev/stdin}"
flush_table
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment