package markdownish import ( "fmt" "strings" "unicode/utf8" ) func escapeMan(raw string) string { b := new(strings.Builder) for len(raw) > 0 { r, size := utf8.DecodeRuneInString(raw) raw = raw[size:] if r < ' ' { r = utf8.RuneError } switch r { case '"': // double quote b.WriteString("\\(dq") case '\'': b.WriteString("\\(cq") case '-': // HYphen //b.WriteString("\\-") b.WriteString("\\(hy") case '|': // bar b.WriteString("\\(ba") case '_': // underline b.WriteString("\\(ul") case '/': // SLash b.WriteString("\\(sl") case '\\': // backwaRd Slash b.WriteString("\\(rs") // TODO escape general unicode // cf. https://mandoc.bsd.lv/man/mandoc_escape.3.html#ESCAPE_UNICODE [accessed 2023-10-21] case '.': // TODO conflicting sources say `\.` is never a good idea / it is the correct escape sequence for the dot //b.WriteString("\\.") b.WriteString("\\&.") case '\u2013': // EN dash b.WriteString("\\(en") case '\u2014': // EM dash b.WriteString("\\(em") // TODO more escape sequences default: b.WriteRune(r) } } return b.String() } type ManConfig struct { Environment map[string]string } func Md2Man(cnf *ManConfig, md *Md) string { b := new(strings.Builder) md2manLineAtoms := func(b *strings.Builder, lineatoms []LineAtom) { var ( danglingBr bool endedInBr bool italic bool bold bool ) for _, lineatom := range lineatoms { endedInBr = false switch la := lineatom.(type) { default: fmt.Fprintf(b, ".\\\" ??? lineatom.(type): %s\n", escapeMan(fmt.Sprintf("%T", lineatom))) // TODO panic(fmt.Sprintf("unreachable: lineatom.(type)=%T", lineatom)) case *Link: _ = la.Text // ignore _ = la.Title // ignore fmt.Fprintf(b, "%s", escapeMan(fmt.Sprintf("<%s>", la.Link))) case *Text: if danglingBr { fmt.Fprintf(b, "\n") endedInBr = true danglingBr = false } // TODO does not seem to work if endedInBr { fmt.Fprintf(b, "\\&") } // TODO this modifies (fixups) *Md if la.Code { la.Italic = false la.Bold = false } if italic != la.Italic || bold != la.Bold { switch { case !la.Italic && !la.Bold: fmt.Fprintf(b, "\\fR") case !la.Italic && la.Bold: fmt.Fprintf(b, "\\fB") case la.Italic && !la.Bold: fmt.Fprintf(b, "\\fI") case la.Italic && la.Bold: fmt.Fprintf(b, "\\fBI") default: panic("unreachable") } italic = la.Italic bold = la.Bold } fmt.Fprintf(b, "%s", escapeMan(la.Text)) case *Br: if danglingBr { fmt.Fprintf(b, "\n.PP\n") endedInBr = true danglingBr = false } else { danglingBr = true } } } if danglingBr || !endedInBr { fmt.Fprintf(b, "\n") danglingBr = false } } md2manAtoms := func(b *strings.Builder, atoms []Atom) { var ( danglingDotRE bool dontStartParagraph bool ) for _, atom := range atoms { switch a := atom.(type) { default: fmt.Fprintf(b, ".\\\" ??? atom.(type): %s\n", escapeMan(fmt.Sprintf("%T", atom))) // TODO panic(fmt.Sprintf("unreachable: atom.(type)=%T", atom)) case *Header: if danglingDotRE { fmt.Fprintf(b, ".RE\n") } danglingDotRE = false switch { case a.Level <= 1: dontStartParagraph = false fmt.Fprintf(b, ".SH %s\n", escapeMan(a.Text)) // [2023-11-07, jfrech] TODO: Currently, any higher level than 2 is treated as 2. case a.Level == 2 || a.Level > 2: dontStartParagraph = false fmt.Fprintf(b, ".SS %s\n", escapeMan(a.Text)) } case *Paragraph: if !dontStartParagraph { fmt.Fprintf(b, ".P\n") } dontStartParagraph = false md2manLineAtoms(b, a.LineAtoms) /* func() { switch { case headerlevel <= 1: // ignore case headerlevel >= 2: fmt.Fprintf(b, ".RS %d\n", headerlevel) defer fmt.Fprintf(b, ".RE\n") } }() */ // cf. https://mandoc.bsd.lv/man/mdoc.7.html#MACRO_OVERVIEW [accessed 2023-11-07] // cf. https://man.netbsd.org/mdoc.samples.7 [accessed 2023-11-07] case *Code: _ = a.Lang // ignore text := a.Text fmt.Fprintf(b, ".P\n") // TODO? //fmt.Fprintf(b, ".Bd -literal -ragged -offset indent-two\n") for len(text) > 0 { k := strings.IndexByte(text, '\n') if k == -1 { fmt.Fprintf(b, "%s\n", escapeMan(text)) text = "" break } fmt.Fprintf(b, "%s\n", escapeMan(text[:k])) text = text[k+1:] } //fmt.Fprintf(b, ".Ed\n") } } } header := make(map[string]string) atoms := md.Atoms if true { for len(atoms) > 0 && IsEmptyParagraph(atoms[0]) { atoms = atoms[1:] } for len(atoms) > 0 { k, v, ok := HeaderExtra(atoms[0]) if !ok { break } atoms = atoms[1:] if header[k] != "" { header[k] += " " } header[k] += v } for len(atoms) > 0 && IsEmptyParagraph(atoms[0]) { atoms = atoms[1:] } // NOTE: only whole-line variable interpretation is supported for k, v := range header { if len(v) < 2 || v[0] != '$' { continue } else if cnf != nil && cnf.Environment == nil { continue } ev, ok := cnf.Environment[v[1:]] if !ok { continue } header[k] = ev } } fmt.Fprintf(b, ".TH \"%s\" \"%s\" \"%s\" \"%s\" \"%s\"\n", escapeMan(header["man:topic"]), escapeMan(header["man:section"]), escapeMan(header["man:date"]), escapeMan(header["man:version"]), escapeMan(header["man:title"]), ) md2manAtoms(b, atoms) return b.String() } /* .TH "BRIEF" "1" "2023\-10\-21" "Brief (devel)" "Brief Manual" .SH "NAME" \fIbrief\fR -- A sincere ... */