Created
June 26, 2025 12:06
-
-
Save stuartc/93eee28d15b627eeba97bc1ff0c3fccc to your computer and use it in GitHub Desktop.
Convert markdown to rich text format for Slack/Discord/etc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
# md2pb - Convert markdown to rich text format for Slack/Discord/etc | |
# | |
# DESCRIPTION: | |
# Converts markdown to both plain text and HTML formats, then places both on the | |
# macOS clipboard. This allows pasting rich formatted text into apps like Slack, | |
# Discord, or any app that accepts HTML clipboard data. | |
# | |
# USAGE: | |
# md2pb [FILE] | |
# echo "**bold text**" | md2pb | |
# md2pb README.md | |
# | |
# REQUIREMENTS: | |
# - macOS (uses osascript and pbcopy) | |
# - pandoc (brew install pandoc) | |
# | |
# EXAMPLES: | |
# # From stdin | |
# echo "# Header\n**Bold** and *italic* text" | md2pb | |
# | |
# # From file | |
# md2pb my-notes.md | |
# | |
# # With llm tool | |
# llm logs -n 1 -r | md2pb | |
# | |
# AUTHOR: Stuart Corbishley | |
# DATE: 2025-06-26 | |
set -euo pipefail | |
# Colors for output | |
RED='\033[0;31m' | |
GREEN='\033[0;32m' | |
YELLOW='\033[1;33m' | |
NC='\033[0m' # No Color | |
# Function to print colored output | |
print_error() { | |
echo -e "${RED}❌ Error: $1${NC}" >&2 | |
} | |
print_success() { | |
echo -e "${GREEN}✅ $1${NC}" | |
} | |
print_warning() { | |
echo -e "${YELLOW}⚠️ Warning: $1${NC}" | |
} | |
print_info() { | |
echo -e "$1" | |
} | |
# Check if we're on macOS | |
check_macos() { | |
if [[ "$OSTYPE" != "darwin"* ]]; then | |
print_error "This script requires macOS (uses osascript and pbcopy)" | |
print_info "Current OS: $OSTYPE" | |
exit 1 | |
fi | |
} | |
# Check if pandoc is installed | |
check_pandoc() { | |
if ! command -v pandoc &>/dev/null; then | |
print_error "pandoc is required but not installed" | |
print_info "" | |
print_info "Install with:" | |
print_info " brew install pandoc" | |
print_info "" | |
print_info "Or visit: https://pandoc.org/installing.html" | |
exit 1 | |
fi | |
} | |
# Show usage | |
show_usage() { | |
echo "Usage: md2pb [OPTIONS] [FILE]" | |
echo "" | |
echo "Convert markdown to rich text format for clipboard" | |
echo "" | |
echo "Options:" | |
echo " --debug-html=FILE Save HTML output to FILE for debugging" | |
echo " --debug-text=FILE Save plain text output to FILE for debugging" | |
echo " --style=STYLE Formatting style: slack (default) or normal" | |
echo " -h, --help Show this help message" | |
echo "" | |
echo "Arguments:" | |
echo " FILE Markdown file to convert (optional, uses stdin if not provided)" | |
echo "" | |
echo "Examples:" | |
echo " echo \"**bold text**\" | md2pb" | |
echo " md2pb README.md" | |
echo " md2pb --style=normal README.md" | |
echo " md2pb --debug-html=output.html --debug-text=output.txt README.md" | |
echo " llm logs -n 1 -r | md2pb" | |
} | |
# Main conversion function | |
convert_markdown_to_clipboard() { | |
local input_source="$1" | |
local debug_html_file="$2" | |
local debug_text_file="$3" | |
local style="$4" | |
local tmpdir="" | |
local tmphtml="" | |
local tmptext="" | |
local tmpfilter="" | |
# Cleanup function | |
cleanup() { | |
rm -rf "${tmpdir:-}" | |
} | |
trap cleanup EXIT | |
# Create temporary directory and files | |
tmpdir=$(mktemp -d /tmp/markdown-clipboard.XXXXXX) | |
tmphtml="$tmpdir/output.html" | |
tmptext="$tmpdir/output.txt" | |
tmpfilter="$tmpdir/filter.lua" | |
# Create lua filter for Slack formatting | |
cat >"$tmpfilter" <<'EOF' | |
function Header(elem) | |
-- Convert headers to bold text for Slack/Discord | |
local content = pandoc.utils.stringify(elem.content) | |
return { | |
pandoc.Para{pandoc.LineBreak()}, | |
pandoc.Para{pandoc.Strong{pandoc.Str(content)}} | |
} | |
end | |
function Para(elem) | |
return { | |
pandoc.Para{pandoc.LineBreak()}, | |
elem | |
} | |
end | |
EOF | |
# Configure markdown format based on style | |
local markdown_extensions=( | |
"markdown" | |
"+lists_without_preceding_blankline" | |
"+strikeout" | |
"+autolink_bare_uris" | |
"+task_lists" | |
"-smart" | |
) | |
local markdown_format | |
markdown_format=$( | |
IFS="" | |
echo "${markdown_extensions[*]}" | |
) | |
local pandoc_html_args=() | |
if [[ "$style" == "slack" ]]; then | |
pandoc_html_args+=("--lua-filter=$tmpfilter") | |
fi | |
pandoc_html_args+=("-t" "html" "--standalone") | |
# Convert to both formats | |
if [[ "$input_source" == "stdin" ]]; then | |
# Read from stdin | |
local content | |
content=$(cat) | |
if [[ -z "$content" ]]; then | |
print_error "No input provided" | |
exit 1 | |
fi | |
echo "$content" | pandoc -f "$markdown_format" -t plain >"$tmptext" | |
echo "$content" | pandoc -f "$markdown_format" "${pandoc_html_args[@]}" >"$tmphtml" | |
else | |
# Read from file | |
if [[ ! -f "$input_source" ]]; then | |
print_error "File not found: $input_source" | |
exit 1 | |
fi | |
if [[ ! -r "$input_source" ]]; then | |
print_error "Cannot read file: $input_source" | |
exit 1 | |
fi | |
pandoc -f "$markdown_format" -t plain "$input_source" >"$tmptext" | |
pandoc -f "$markdown_format" "${pandoc_html_args[@]}" "$input_source" >"$tmphtml" | |
fi | |
# Set plain text version first (this is the key!) | |
if ! pbcopy <"$tmptext"; then | |
print_error "Failed to copy plain text to clipboard" | |
exit 1 | |
fi | |
# Add HTML format to existing clipboard | |
if ! osascript \ | |
-e "set htmlData to (read POSIX file \"$tmphtml\" as «class HTML»)" \ | |
-e 'set currentClip to the clipboard as record' \ | |
-e 'set the clipboard to (currentClip & {«class HTML»:htmlData})' \ | |
2>/dev/null; then | |
print_error "Failed to add HTML format to clipboard" | |
exit 1 | |
fi | |
# Save HTML to debug file if requested | |
if [[ -n "$debug_html_file" ]]; then | |
if cp "$tmphtml" "$debug_html_file"; then | |
print_info "🐛 HTML debug output saved to: $debug_html_file" | |
else | |
print_warning "Failed to save HTML debug output to: $debug_html_file" | |
fi | |
fi | |
# Save plain text to debug file if requested | |
if [[ -n "$debug_text_file" ]]; then | |
if cp "$tmptext" "$debug_text_file"; then | |
print_info "🐛 Plain text debug output saved to: $debug_text_file" | |
else | |
print_warning "Failed to save plain text debug output to: $debug_text_file" | |
fi | |
fi | |
# Show success message with preview | |
local line_count word_count | |
line_count=$(wc -l <"$tmptext" | tr -d ' ') | |
word_count=$(wc -w <"$tmptext" | tr -d ' ') | |
print_success "Markdown converted and copied to clipboard" | |
print_info "📊 Stats: $line_count lines, $word_count words" | |
print_info "📋 Ready to paste into Slack, Discord, or other rich text apps" | |
} | |
# Main script logic | |
main() { | |
local debug_html_file="" | |
local debug_text_file="" | |
local input_file="" | |
local style="slack" | |
# Parse command line arguments | |
while [[ $# -gt 0 ]]; do | |
case $1 in | |
-h | --help | help) | |
show_usage | |
exit 0 | |
;; | |
--debug-html=*) | |
debug_html_file="${1#*=}" | |
if [[ -z "$debug_html_file" ]]; then | |
print_error "--debug-html requires a filename" | |
exit 1 | |
fi | |
shift | |
;; | |
--debug-text=*) | |
debug_text_file="${1#*=}" | |
if [[ -z "$debug_text_file" ]]; then | |
print_error "--debug-text requires a filename" | |
exit 1 | |
fi | |
shift | |
;; | |
--style=*) | |
style="${1#*=}" | |
if [[ "$style" != "slack" && "$style" != "normal" ]]; then | |
print_error "Style must be 'slack' or 'normal', got: $style" | |
exit 1 | |
fi | |
shift | |
;; | |
-*) | |
print_error "Unknown option: $1" | |
echo "" | |
show_usage | |
exit 1 | |
;; | |
*) | |
if [[ -n "$input_file" ]]; then | |
print_error "Too many input files specified" | |
echo "" | |
show_usage | |
exit 1 | |
fi | |
input_file="$1" | |
shift | |
;; | |
esac | |
done | |
# Check system requirements | |
check_macos | |
check_pandoc | |
# Determine input source | |
if [[ -z "$input_file" ]]; then | |
# No input file, check if stdin has data | |
if [[ -t 0 ]]; then | |
print_error "No input provided" | |
echo "" | |
show_usage | |
exit 1 | |
fi | |
convert_markdown_to_clipboard "stdin" "$debug_html_file" "$debug_text_file" "$style" | |
else | |
# Input file specified | |
convert_markdown_to_clipboard "$input_file" "$debug_html_file" "$debug_text_file" "$style" | |
fi | |
} | |
# Run main function with all arguments | |
main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment