codehakase.com

Parsing, Not Guessing

June 14, 2025

Markdown rendering in the terminal is usually done with string replacements or regex. This doesn’t scale. Goldmark exposes a proper AST. Each node has type, position, and content. Rendering becomes walking the tree:

// render renders the AST node to the writer
func (tr *terminalRenderer) render(w io.Writer, source []byte, node ast.Node) error {
	return ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
		err := tr.renderNode(w, source, node, entering)
		if err != nil {
			return ast.WalkStop, err
		}
		return ast.WalkContinue, nil
	})
}

This is better. You don’t need to guess whether a * means emphasis or a list. You know.

Syntax highlighting is handled by Chroma. It doesn’t support aliases like jsonc, tsx, zsh. I added a switch. Not ideal, but works:

// getLexerByAlias handles language aliases that might not be directly supported
func (ch *ChromaHelper) getLexerByAlias(language string) chroma.Lexer {
	switch strings.ToLower(language) {
	case "jsonc", "json5":
		return lexers.Get("json")
	case "tsx":
		return lexers.Get("typescript")
	case "jsx":
		return lexers.Get("javascript")
	case "sh", "shell", "zsh", "fish":
		return lexers.Get("bash")
    ...
	default:
		return nil
	}
}

Terminal themes: there is no portable API. Some terminals expose COLORFGBG. Others don’t. Parse it when it exists:

func DetectTerminalBackground() BackgroundType {
	if theme := os.Getenv("COLORFGBG"); theme != "" {
		// COLORFGBG format is typically "foreground;background"
		// Lower numbers (0-7) typically indicate darker colors
		// Higher numbers (8-15) typically indicate lighter colors
		parts := strings.Split(theme, ";")
		if len(parts) >= 2 {
			bg := parts[len(parts)-1]
			// Simple heuristic: if background is 0-7, it's likely dark
			if bg >= "0" && bg <= "7" {
				return BackgroundDark
			} else if bg >= "8" && bg <= "15" {
				return BackgroundLight
			}
		}
	}
    
    //...
}

Pager integration uses less:

// Display shows the content using less with vim-style navigation options
func (p *Pager) Display(content string) error {
	if p.lessPath == "" {
		return fmt.Errorf("less command not available")
	}

	args := []string{
		"-R", // Raw control characters (for ANSI colors)
		"-S", // Chop long lines (don't wrap)
		"-X", // Don't clear screen on exit
		"-F", // Quit if entire file fits on screen
		"-K", // Exit on Ctrl-C
		"+g", // Start at beginning (gg equivalent)
	}

	env := append(os.Environ(),
		"LESS_TERMCAP_md=\033[1;36m",    // Bold cyan for headings
		"LESS_TERMCAP_us=\033[1;32m",    // Bold green for underline
		"LESS_TERMCAP_so=\033[1;44;33m", // Bold yellow on blue for standout
		"LESS_TERMCAP_se=\033[0m",       // End standout
		"LESS_TERMCAP_ue=\033[0m",       // End underline
		"LESS_TERMCAP_me=\033[0m",       // End bold/italic
	)

	cmd := exec.Command(p.lessPath, args...)
	cmd.Env = env
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

    // ...

	return nil
}

These are some of my notes from a recent tool I worked on called md. It renders Markdown in the terminal with syntax highlighting and theme support. Not the point. The point is: parse, don’t guess.

Tags: