// Copyright 2022 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.package commentimport ()// A textPrinter holds the state needed for printing a Doc as plain text.type textPrinter struct { *Printer long strings.Builder prefix string codePrefix string width int}// Text returns a textual formatting of the [Doc].// See the [Printer] documentation for ways to customize the text output.func ( *Printer) ( *Doc) []byte { := &textPrinter{Printer: ,prefix: .TextPrefix,codePrefix: .TextCodePrefix,width: .TextWidth, }if .codePrefix == "" { .codePrefix = .TextPrefix + "\t" }if .width == 0 { .width = 80 - utf8.RuneCountInString(.prefix) }varbytes.Bufferfor , := range .Content {if > 0 && blankBefore() { .WriteString(.prefix)writeNL(&) } .block(&, ) } := falsefor , := range .Links {if .Used { = truebreak } }if {writeNL(&)for , := range .Links {if .Used {fmt.Fprintf(&, "[%s]: %s\n", .Text, .URL) } } }return .Bytes()}// writeNL calls out.WriteByte('\n')// but first trims trailing spaces on the previous line.func writeNL( *bytes.Buffer) {// Trim trailing spaces. := .Bytes() := 0for < len() && ([len()--1] == ' ' || [len()--1] == '\t') { ++ }if > 0 { .Truncate(len() - ) } .WriteByte('\n')}// block prints the block x to out.func ( *textPrinter) ( *bytes.Buffer, Block) {switch x := .(type) {default:fmt.Fprintf(, "?%T\n", )case *Paragraph: .WriteString(.prefix) .text(, "", .Text)case *Heading: .WriteString(.prefix) .WriteString("# ") .text(, "", .Text)case *Code: := .Textfor != "" {varstring , , _ = strings.Cut(, "\n")if != "" { .WriteString(.codePrefix) .WriteString() }writeNL() }case *List: := .BlankBetween()for , := range .Items {if > 0 && { .WriteString(.prefix)writeNL() } .WriteString(.prefix) .WriteString(" ")if .Number == "" { .WriteString(" - ") } else { .WriteString(.Number) .WriteString(". ") }for , := range .Content {const = " "if > 0 {writeNL() .WriteString(.prefix) .WriteString() } .text(, , .(*Paragraph).Text) } } }}// text prints the text sequence x to out.func ( *textPrinter) ( *bytes.Buffer, string, []Text) { .oneLongLine(&.long, ) := strings.Fields(.long.String()) .long.Reset()var []intif .width < 0 || len() == 0 { = []int{0, len()} // one long line } else { = wrap(, .width-utf8.RuneCountInString()) }for := 0; +1 < len(); ++ {if > 0 { .WriteString(.prefix) .WriteString() }for , := range [[]:[+1]] {if > 0 { .WriteString(" ") } .WriteString() }writeNL() }}// oneLongLine prints the text sequence x to out as one long line,// without worrying about line wrapping.// Explicit links have the [ ] dropped to improve readability.func ( *textPrinter) ( *strings.Builder, []Text) {for , := range {switch t := .(type) {casePlain: .WriteString(string())caseItalic: .WriteString(string())case *Link: .(, .Text)case *DocLink: .(, .Text) } }}// wrap wraps words into lines of at most max runes,// minimizing the sum of the squares of the leftover lengths// at the end of each line (except the last, of course),// with a preference for ending lines at punctuation (.,:;).//// The returned slice gives the indexes of the first words// on each line in the wrapped text with a final entry of len(words).// Thus the lines are words[seq[0]:seq[1]], words[seq[1]:seq[2]],// ..., words[seq[len(seq)-2]:seq[len(seq)-1]].//// The implementation runs in O(n log n) time, where n = len(words),// using the algorithm described in D. S. Hirschberg and L. L. Larmore,// “[The least weight subsequence problem],” FOCS 1985, pp. 137-143.//// [The least weight subsequence problem]: https://doi.org/10.1109/SFCS.1985.60func wrap( []string, int) ( []int) {// The algorithm requires that our scoring function be concave, // meaning that for all i₀ ≤ i₁ < j₀ ≤ j₁, // weight(i₀, j₀) + weight(i₁, j₁) ≤ weight(i₀, j₁) + weight(i₁, j₀). // // Our weights are two-element pairs [hi, lo] // ordered by elementwise comparison. // The hi entry counts the weight for lines that are longer than max, // and the lo entry counts the weight for lines that are not. // This forces the algorithm to first minimize the number of lines // that are longer than max, which correspond to lines with // single very long words. Having done that, it can move on to // minimizing the lo score, which is more interesting. // // The lo score is the sum for each line of the square of the // number of spaces remaining at the end of the line and a // penalty of 64 given out for not ending the line in a // punctuation character (.,:;). // The penalty is somewhat arbitrarily chosen by trying // different amounts and judging how nice the wrapped text looks. // Roughly speaking, using 64 means that we are willing to // end a line with eight blank spaces in order to end at a // punctuation character, even if the next word would fit in // those spaces. // // We care about ending in punctuation characters because // it makes the text easier to skim if not too many sentences // or phrases begin with a single word on the previous line.// A score is the score (also called weight) for a given line. // add and cmp add and compare scores.typestruct {int64int64 } := func(, ) { return {. + ., . + .} } := func(, ) int {switch {case . < .:return -1case . > .:return +1case . < .:return -1case . > .:return +1 }return0 }// total[j] is the total number of runes // (including separating spaces) in words[:j]. := make([]int, len()+1) [0] = 0for , := range { [1+] = [] + utf8.RuneCountInString() + 1 }// weight returns weight(i, j). := func(, int) {// On the last line, there is zero weight for being too short. := [] - 1 - []if == len() && <= {return {0, 0} }// Otherwise the weight is the penalty plus the square of the number of // characters remaining on the line or by which the line goes over. // In the latter case, that value goes in the hi part of the score. // (See note above.) := wrapPenalty([-1]) := int64(-) * int64(-)if > {return {, } }return {0, + } }// The rest of this function is “The Basic Algorithm” from // Hirschberg and Larmore's conference paper, // using the same names as in the paper. := []{{0, 0}} := func(, int) { return ([], (, )) } := func(, , int) bool { := + sort.Search(len()+1-, func( int) bool { += return ((, ), (, )) > 0 })if > len() {returntrue }return ((, ), (, )) <= 0 }// d is a one-ended deque implemented as a slice. := make([]int, 1, len()) [0] = 0 := make([]int, 1, len()) [0] = -1for := 1; < len(); ++ { = append(, ([0], )) = append(, [0])forlen() > 1 && (([1], +1), ([0], +1)) <= 0 { = [1:] // “Retire” }forlen() > 1 && ([len()-2], [len()-1], ) { = [:len()-1] // “Fire” }if ((, len()), ([len()-1], len())) < 0 { = append(, ) // “Hire”// The next few lines are not in the paper but are necessary // to handle two-word inputs correctly. It appears to be // just a bug in the paper's pseudocode.iflen() == 2 && (([1], +1), ([0], +1)) <= 0 { = [1:] } } } = append(, [0])// Recover least weight sequence from bestleft. := 1for := len(); > 0; = [] { ++ } = make([]int, )for := len(); > 0; = [] { -- [] = }return}// wrapPenalty is the penalty for inserting a line break after word s.func wrapPenalty( string) int64 {switch [len()-1] {case'.', ',', ':', ';':return0 }return64}
The pages are generated with Goldsv0.7.0-preview. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.