chroma-markdown/main.go

157 lines
3.6 KiB
Go

package main
import (
"bufio"
"bytes"
"flag"
"fmt"
"io"
"os"
"os/exec"
"strings"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
)
// dataPipedIn returns true if the user piped data via stdin.
func dataPipedIn() bool {
stat, err := os.Stdin.Stat()
if err != nil {
return false
}
return (stat.Mode() & os.ModeCharDevice) == 0
}
func checkError(err error, msg string) {
if err != nil {
if msg != "" {
fmt.Fprintf(os.Stderr, "Error %s: %v\n", msg, err)
} else {
fmt.Fprintf(os.Stderr, "Encountered an error: %v", err)
}
os.Exit(2)
}
}
func init() {
flag.Usage = func() {
os.Stderr.WriteString(`usage: chroma-markdown [markdown-file]
`)
flag.PrintDefaults()
}
}
func highlight(w io.Writer, source, lexer, style string) error {
// Determine lexer.
l := lexers.Get(lexer)
if l == nil {
l = lexers.Analyse(source)
}
if l == nil {
l = lexers.Fallback
}
l = chroma.Coalesce(l)
// Determine formatter.
f := html.New(html.WithClasses(true), html.TabWidth(4))
// Determine style.
s := styles.Get(style)
if s == nil {
s = styles.Fallback
}
it, err := l.Tokenise(nil, source)
if err != nil {
return err
}
return f.Format(w, s, it)
}
const Version = "0.0"
func main() {
css := flag.String("css", "", "Path to a CSS import to include at the beginning of the output")
style := flag.String("style", "native", "CSS style to use")
version := flag.Bool("version", false, "Print the version string")
v := flag.Bool("v", false, "Print the version string")
flag.Parse()
if *version || *v {
fmt.Printf("chroma-markdown version %s\n", Version)
os.Exit(2)
}
var r io.Reader
if dataPipedIn() {
r = os.Stdin
} else {
if flag.NArg() != 1 {
flag.Usage()
}
file := flag.Arg(0)
f, err := os.Open(file)
checkError(err, "opening file")
defer f.Close()
r = bufio.NewReader(f)
}
out := new(bytes.Buffer)
currentCodeBlock := new(bytes.Buffer)
started := false
bs := bufio.NewScanner(r)
lang := ""
needCSS := false
for bs.Scan() {
text := bs.Text()
trimmed := strings.TrimSpace(text)
if strings.HasPrefix(trimmed, "```") {
if started {
// TODO: compile the code block to markdown
quickErr := highlight(out, currentCodeBlock.String(), lang, *style)
checkError(quickErr, "highlighting source code")
started = false
currentCodeBlock.Reset()
lang = ""
needCSS = true
continue
}
lang = trimmed[3:]
started = true
continue
}
if started {
currentCodeBlock.WriteString(text)
currentCodeBlock.WriteByte('\n')
} else {
out.WriteString(text)
out.WriteByte('\n')
}
}
checkError(bs.Err(), "reading markdown file")
f, err := os.CreateTemp("", "chroma-markdown-")
checkError(err, "creating temporary file")
w := bufio.NewWriter(f)
if needCSS && *css != "" {
_, writeErr := fmt.Fprintf(w, `<link rel="stylesheet" type="text/css" href="%s" />\n`, *css)
checkError(writeErr, "writing data to temporary file")
}
_, writeErr := f.Write(out.Bytes())
checkError(writeErr, "writing data to temporary file")
// shell out to markdown because of
// https://github.com/russross/blackfriday/issues/403
cmark, lookErr := exec.LookPath("cmark")
args := []string{cmark, "--unsafe", f.Name()}
if lookErr != nil {
cmark, lookErr = exec.LookPath("markdown")
checkError(lookErr, "finding markdown binary")
args = []string{cmark, f.Name()}
}
execErr := localExec(cmark, args, []string{})
checkError(execErr, "executing markdown binary")
if err := f.Close(); err != nil {
checkError(err, "closing file")
}
}