whatcanGOwrong
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
// 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 diff
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A pair is a pair of values tracked for both the x and y side of a diff.
|
||||
// It is typically a pair of line indexes.
|
||||
type pair struct{ x, y int }
|
||||
|
||||
// Diff returns an anchored diff of the two texts old and new
|
||||
// in the “unified diff” format. If old and new are identical,
|
||||
// Diff returns a nil slice (no output).
|
||||
//
|
||||
// Unix diff implementations typically look for a diff with
|
||||
// the smallest number of lines inserted and removed,
|
||||
// which can in the worst case take time quadratic in the
|
||||
// number of lines in the texts. As a result, many implementations
|
||||
// either can be made to run for a long time or cut off the search
|
||||
// after a predetermined amount of work.
|
||||
//
|
||||
// In contrast, this implementation looks for a diff with the
|
||||
// smallest number of “unique” lines inserted and removed,
|
||||
// where unique means a line that appears just once in both old and new.
|
||||
// We call this an “anchored diff” because the unique lines anchor
|
||||
// the chosen matching regions. An anchored diff is usually clearer
|
||||
// than a standard diff, because the algorithm does not try to
|
||||
// reuse unrelated blank lines or closing braces.
|
||||
// The algorithm also guarantees to run in O(n log n) time
|
||||
// instead of the standard O(n²) time.
|
||||
//
|
||||
// Some systems call this approach a “patience diff,” named for
|
||||
// the “patience sorting” algorithm, itself named for a solitaire card game.
|
||||
// We avoid that name for two reasons. First, the name has been used
|
||||
// for a few different variants of the algorithm, so it is imprecise.
|
||||
// Second, the name is frequently interpreted as meaning that you have
|
||||
// to wait longer (to be patient) for the diff, meaning that it is a slower algorithm,
|
||||
// when in fact the algorithm is faster than the standard one.
|
||||
func Diff(oldName string, old []byte, newName string, new []byte) []byte {
|
||||
if bytes.Equal(old, new) {
|
||||
return nil
|
||||
}
|
||||
x := lines(old)
|
||||
y := lines(new)
|
||||
|
||||
// Print diff header.
|
||||
var out bytes.Buffer
|
||||
fmt.Fprintf(&out, "diff %s %s\n", oldName, newName)
|
||||
fmt.Fprintf(&out, "--- %s\n", oldName)
|
||||
fmt.Fprintf(&out, "+++ %s\n", newName)
|
||||
|
||||
// Loop over matches to consider,
|
||||
// expanding each match to include surrounding lines,
|
||||
// and then printing diff chunks.
|
||||
// To avoid setup/teardown cases outside the loop,
|
||||
// tgs returns a leading {0,0} and trailing {len(x), len(y)} pair
|
||||
// in the sequence of matches.
|
||||
var (
|
||||
done pair // printed up to x[:done.x] and y[:done.y]
|
||||
chunk pair // start lines of current chunk
|
||||
count pair // number of lines from each side in current chunk
|
||||
ctext []string // lines for current chunk
|
||||
)
|
||||
for _, m := range tgs(x, y) {
|
||||
if m.x < done.x {
|
||||
// Already handled scanning forward from earlier match.
|
||||
continue
|
||||
}
|
||||
|
||||
// Expand matching lines as far as possible,
|
||||
// establishing that x[start.x:end.x] == y[start.y:end.y].
|
||||
// Note that on the first (or last) iteration we may (or definitely do)
|
||||
// have an empty match: start.x==end.x and start.y==end.y.
|
||||
start := m
|
||||
for start.x > done.x && start.y > done.y && x[start.x-1] == y[start.y-1] {
|
||||
start.x--
|
||||
start.y--
|
||||
}
|
||||
end := m
|
||||
for end.x < len(x) && end.y < len(y) && x[end.x] == y[end.y] {
|
||||
end.x++
|
||||
end.y++
|
||||
}
|
||||
|
||||
// Emit the mismatched lines before start into this chunk.
|
||||
// (No effect on first sentinel iteration, when start = {0,0}.)
|
||||
for _, s := range x[done.x:start.x] {
|
||||
ctext = append(ctext, "-"+s)
|
||||
count.x++
|
||||
}
|
||||
for _, s := range y[done.y:start.y] {
|
||||
ctext = append(ctext, "+"+s)
|
||||
count.y++
|
||||
}
|
||||
|
||||
// If we're not at EOF and have too few common lines,
|
||||
// the chunk includes all the common lines and continues.
|
||||
const C = 3 // number of context lines
|
||||
if (end.x < len(x) || end.y < len(y)) &&
|
||||
(end.x-start.x < C || (len(ctext) > 0 && end.x-start.x < 2*C)) {
|
||||
for _, s := range x[start.x:end.x] {
|
||||
ctext = append(ctext, " "+s)
|
||||
count.x++
|
||||
count.y++
|
||||
}
|
||||
done = end
|
||||
continue
|
||||
}
|
||||
|
||||
// End chunk with common lines for context.
|
||||
if len(ctext) > 0 {
|
||||
n := end.x - start.x
|
||||
if n > C {
|
||||
n = C
|
||||
}
|
||||
for _, s := range x[start.x : start.x+n] {
|
||||
ctext = append(ctext, " "+s)
|
||||
count.x++
|
||||
count.y++
|
||||
}
|
||||
done = pair{start.x + n, start.y + n}
|
||||
|
||||
// Format and emit chunk.
|
||||
// Convert line numbers to 1-indexed.
|
||||
// Special case: empty file shows up as 0,0 not 1,0.
|
||||
if count.x > 0 {
|
||||
chunk.x++
|
||||
}
|
||||
if count.y > 0 {
|
||||
chunk.y++
|
||||
}
|
||||
fmt.Fprintf(&out, "@@ -%d,%d +%d,%d @@\n", chunk.x, count.x, chunk.y, count.y)
|
||||
for _, s := range ctext {
|
||||
out.WriteString(s)
|
||||
}
|
||||
count.x = 0
|
||||
count.y = 0
|
||||
ctext = ctext[:0]
|
||||
}
|
||||
|
||||
// If we reached EOF, we're done.
|
||||
if end.x >= len(x) && end.y >= len(y) {
|
||||
break
|
||||
}
|
||||
|
||||
// Otherwise start a new chunk.
|
||||
chunk = pair{end.x - C, end.y - C}
|
||||
for _, s := range x[chunk.x:end.x] {
|
||||
ctext = append(ctext, " "+s)
|
||||
count.x++
|
||||
count.y++
|
||||
}
|
||||
done = end
|
||||
}
|
||||
|
||||
return out.Bytes()
|
||||
}
|
||||
|
||||
// lines returns the lines in the file x, including newlines.
|
||||
// If the file does not end in a newline, one is supplied
|
||||
// along with a warning about the missing newline.
|
||||
func lines(x []byte) []string {
|
||||
l := strings.SplitAfter(string(x), "\n")
|
||||
if l[len(l)-1] == "" {
|
||||
l = l[:len(l)-1]
|
||||
} else {
|
||||
// Treat last line as having a message about the missing newline attached,
|
||||
// using the same text as BSD/GNU diff (including the leading backslash).
|
||||
l[len(l)-1] += "\n\\ No newline at end of file\n"
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// tgs returns the pairs of indexes of the longest common subsequence
|
||||
// of unique lines in x and y, where a unique line is one that appears
|
||||
// once in x and once in y.
|
||||
//
|
||||
// The longest common subsequence algorithm is as described in
|
||||
// Thomas G. Szymanski, “A Special Case of the Maximal Common
|
||||
// Subsequence Problem,” Princeton TR #170 (January 1975),
|
||||
// available at https://research.swtch.com/tgs170.pdf.
|
||||
func tgs(x, y []string) []pair {
|
||||
// Count the number of times each string appears in a and b.
|
||||
// We only care about 0, 1, many, counted as 0, -1, -2
|
||||
// for the x side and 0, -4, -8 for the y side.
|
||||
// Using negative numbers now lets us distinguish positive line numbers later.
|
||||
m := make(map[string]int)
|
||||
for _, s := range x {
|
||||
if c := m[s]; c > -2 {
|
||||
m[s] = c - 1
|
||||
}
|
||||
}
|
||||
for _, s := range y {
|
||||
if c := m[s]; c > -8 {
|
||||
m[s] = c - 4
|
||||
}
|
||||
}
|
||||
|
||||
// Now unique strings can be identified by m[s] = -1+-4.
|
||||
//
|
||||
// Gather the indexes of those strings in x and y, building:
|
||||
// xi[i] = increasing indexes of unique strings in x.
|
||||
// yi[i] = increasing indexes of unique strings in y.
|
||||
// inv[i] = index j such that x[xi[i]] = y[yi[j]].
|
||||
var xi, yi, inv []int
|
||||
for i, s := range y {
|
||||
if m[s] == -1+-4 {
|
||||
m[s] = len(yi)
|
||||
yi = append(yi, i)
|
||||
}
|
||||
}
|
||||
for i, s := range x {
|
||||
if j, ok := m[s]; ok && j >= 0 {
|
||||
xi = append(xi, i)
|
||||
inv = append(inv, j)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply Algorithm A from Szymanski's paper.
|
||||
// In those terms, A = J = inv and B = [0, n).
|
||||
// We add sentinel pairs {0,0}, and {len(x),len(y)}
|
||||
// to the returned sequence, to help the processing loop.
|
||||
J := inv
|
||||
n := len(xi)
|
||||
T := make([]int, n)
|
||||
L := make([]int, n)
|
||||
for i := range T {
|
||||
T[i] = n + 1
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
k := sort.Search(n, func(k int) bool {
|
||||
return T[k] >= J[i]
|
||||
})
|
||||
T[k] = J[i]
|
||||
L[i] = k + 1
|
||||
}
|
||||
k := 0
|
||||
for _, v := range L {
|
||||
if k < v {
|
||||
k = v
|
||||
}
|
||||
}
|
||||
seq := make([]pair, 2+k)
|
||||
seq[1+k] = pair{len(x), len(y)} // sentinel at end
|
||||
lastj := n
|
||||
for i := n - 1; i >= 0; i-- {
|
||||
if L[i] == k && J[i] < lastj {
|
||||
seq[k] = pair{xi[i], yi[J[i]]}
|
||||
k--
|
||||
}
|
||||
}
|
||||
seq[0] = pair{0, 0} // sentinel at start
|
||||
return seq
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// 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 comment implements parsing and reformatting of Go doc comments,
|
||||
(documentation comments), which are comments that immediately precede
|
||||
a top-level declaration of a package, const, func, type, or var.
|
||||
|
||||
Go doc comment syntax is a simplified subset of Markdown that supports
|
||||
links, headings, paragraphs, lists (without nesting), and preformatted text blocks.
|
||||
The details of the syntax are documented at https://go.dev/doc/comment.
|
||||
|
||||
To parse the text associated with a doc comment (after removing comment markers),
|
||||
use a [Parser]:
|
||||
|
||||
var p comment.Parser
|
||||
doc := p.Parse(text)
|
||||
|
||||
The result is a [*Doc].
|
||||
To reformat it as a doc comment, HTML, Markdown, or plain text,
|
||||
use a [Printer]:
|
||||
|
||||
var pr comment.Printer
|
||||
os.Stdout.Write(pr.Text(doc))
|
||||
|
||||
The [Parser] and [Printer] types are structs whose fields can be
|
||||
modified to customize the operations.
|
||||
For details, see the documentation for those types.
|
||||
|
||||
Use cases that need additional control over reformatting can
|
||||
implement their own logic by inspecting the parsed syntax itself.
|
||||
See the documentation for [Doc], [Block], [Text] for an overview
|
||||
and links to additional types.
|
||||
*/
|
||||
package comment
|
||||
@@ -0,0 +1,169 @@
|
||||
// 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 comment
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// An htmlPrinter holds the state needed for printing a [Doc] as HTML.
|
||||
type htmlPrinter struct {
|
||||
*Printer
|
||||
tight bool
|
||||
}
|
||||
|
||||
// HTML returns an HTML formatting of the [Doc].
|
||||
// See the [Printer] documentation for ways to customize the HTML output.
|
||||
func (p *Printer) HTML(d *Doc) []byte {
|
||||
hp := &htmlPrinter{Printer: p}
|
||||
var out bytes.Buffer
|
||||
for _, x := range d.Content {
|
||||
hp.block(&out, x)
|
||||
}
|
||||
return out.Bytes()
|
||||
}
|
||||
|
||||
// block prints the block x to out.
|
||||
func (p *htmlPrinter) block(out *bytes.Buffer, x Block) {
|
||||
switch x := x.(type) {
|
||||
default:
|
||||
fmt.Fprintf(out, "?%T", x)
|
||||
|
||||
case *Paragraph:
|
||||
if !p.tight {
|
||||
out.WriteString("<p>")
|
||||
}
|
||||
p.text(out, x.Text)
|
||||
out.WriteString("\n")
|
||||
|
||||
case *Heading:
|
||||
out.WriteString("<h")
|
||||
h := strconv.Itoa(p.headingLevel())
|
||||
out.WriteString(h)
|
||||
if id := p.headingID(x); id != "" {
|
||||
out.WriteString(` id="`)
|
||||
p.escape(out, id)
|
||||
out.WriteString(`"`)
|
||||
}
|
||||
out.WriteString(">")
|
||||
p.text(out, x.Text)
|
||||
out.WriteString("</h")
|
||||
out.WriteString(h)
|
||||
out.WriteString(">\n")
|
||||
|
||||
case *Code:
|
||||
out.WriteString("<pre>")
|
||||
p.escape(out, x.Text)
|
||||
out.WriteString("</pre>\n")
|
||||
|
||||
case *List:
|
||||
kind := "ol>\n"
|
||||
if x.Items[0].Number == "" {
|
||||
kind = "ul>\n"
|
||||
}
|
||||
out.WriteString("<")
|
||||
out.WriteString(kind)
|
||||
next := "1"
|
||||
for _, item := range x.Items {
|
||||
out.WriteString("<li")
|
||||
if n := item.Number; n != "" {
|
||||
if n != next {
|
||||
out.WriteString(` value="`)
|
||||
out.WriteString(n)
|
||||
out.WriteString(`"`)
|
||||
next = n
|
||||
}
|
||||
next = inc(next)
|
||||
}
|
||||
out.WriteString(">")
|
||||
p.tight = !x.BlankBetween()
|
||||
for _, blk := range item.Content {
|
||||
p.block(out, blk)
|
||||
}
|
||||
p.tight = false
|
||||
}
|
||||
out.WriteString("</")
|
||||
out.WriteString(kind)
|
||||
}
|
||||
}
|
||||
|
||||
// inc increments the decimal string s.
|
||||
// For example, inc("1199") == "1200".
|
||||
func inc(s string) string {
|
||||
b := []byte(s)
|
||||
for i := len(b) - 1; i >= 0; i-- {
|
||||
if b[i] < '9' {
|
||||
b[i]++
|
||||
return string(b)
|
||||
}
|
||||
b[i] = '0'
|
||||
}
|
||||
return "1" + string(b)
|
||||
}
|
||||
|
||||
// text prints the text sequence x to out.
|
||||
func (p *htmlPrinter) text(out *bytes.Buffer, x []Text) {
|
||||
for _, t := range x {
|
||||
switch t := t.(type) {
|
||||
case Plain:
|
||||
p.escape(out, string(t))
|
||||
case Italic:
|
||||
out.WriteString("<i>")
|
||||
p.escape(out, string(t))
|
||||
out.WriteString("</i>")
|
||||
case *Link:
|
||||
out.WriteString(`<a href="`)
|
||||
p.escape(out, t.URL)
|
||||
out.WriteString(`">`)
|
||||
p.text(out, t.Text)
|
||||
out.WriteString("</a>")
|
||||
case *DocLink:
|
||||
url := p.docLinkURL(t)
|
||||
if url != "" {
|
||||
out.WriteString(`<a href="`)
|
||||
p.escape(out, url)
|
||||
out.WriteString(`">`)
|
||||
}
|
||||
p.text(out, t.Text)
|
||||
if url != "" {
|
||||
out.WriteString("</a>")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// escape prints s to out as plain text,
|
||||
// escaping < & " ' and > to avoid being misinterpreted
|
||||
// in larger HTML constructs.
|
||||
func (p *htmlPrinter) escape(out *bytes.Buffer, s string) {
|
||||
start := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '<':
|
||||
out.WriteString(s[start:i])
|
||||
out.WriteString("<")
|
||||
start = i + 1
|
||||
case '&':
|
||||
out.WriteString(s[start:i])
|
||||
out.WriteString("&")
|
||||
start = i + 1
|
||||
case '"':
|
||||
out.WriteString(s[start:i])
|
||||
out.WriteString(""")
|
||||
start = i + 1
|
||||
case '\'':
|
||||
out.WriteString(s[start:i])
|
||||
out.WriteString("'")
|
||||
start = i + 1
|
||||
case '>':
|
||||
out.WriteString(s[start:i])
|
||||
out.WriteString(">")
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
out.WriteString(s[start:])
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
// 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 comment
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// An mdPrinter holds the state needed for printing a Doc as Markdown.
|
||||
type mdPrinter struct {
|
||||
*Printer
|
||||
headingPrefix string
|
||||
raw bytes.Buffer
|
||||
}
|
||||
|
||||
// Markdown returns a Markdown formatting of the Doc.
|
||||
// See the [Printer] documentation for ways to customize the Markdown output.
|
||||
func (p *Printer) Markdown(d *Doc) []byte {
|
||||
mp := &mdPrinter{
|
||||
Printer: p,
|
||||
headingPrefix: strings.Repeat("#", p.headingLevel()) + " ",
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
for i, x := range d.Content {
|
||||
if i > 0 {
|
||||
out.WriteByte('\n')
|
||||
}
|
||||
mp.block(&out, x)
|
||||
}
|
||||
return out.Bytes()
|
||||
}
|
||||
|
||||
// block prints the block x to out.
|
||||
func (p *mdPrinter) block(out *bytes.Buffer, x Block) {
|
||||
switch x := x.(type) {
|
||||
default:
|
||||
fmt.Fprintf(out, "?%T", x)
|
||||
|
||||
case *Paragraph:
|
||||
p.text(out, x.Text)
|
||||
out.WriteString("\n")
|
||||
|
||||
case *Heading:
|
||||
out.WriteString(p.headingPrefix)
|
||||
p.text(out, x.Text)
|
||||
if id := p.headingID(x); id != "" {
|
||||
out.WriteString(" {#")
|
||||
out.WriteString(id)
|
||||
out.WriteString("}")
|
||||
}
|
||||
out.WriteString("\n")
|
||||
|
||||
case *Code:
|
||||
md := x.Text
|
||||
for md != "" {
|
||||
var line string
|
||||
line, md, _ = strings.Cut(md, "\n")
|
||||
if line != "" {
|
||||
out.WriteString("\t")
|
||||
out.WriteString(line)
|
||||
}
|
||||
out.WriteString("\n")
|
||||
}
|
||||
|
||||
case *List:
|
||||
loose := x.BlankBetween()
|
||||
for i, item := range x.Items {
|
||||
if i > 0 && loose {
|
||||
out.WriteString("\n")
|
||||
}
|
||||
if n := item.Number; n != "" {
|
||||
out.WriteString(" ")
|
||||
out.WriteString(n)
|
||||
out.WriteString(". ")
|
||||
} else {
|
||||
out.WriteString(" - ") // SP SP - SP
|
||||
}
|
||||
for i, blk := range item.Content {
|
||||
const fourSpace = " "
|
||||
if i > 0 {
|
||||
out.WriteString("\n" + fourSpace)
|
||||
}
|
||||
p.text(out, blk.(*Paragraph).Text)
|
||||
out.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// text prints the text sequence x to out.
|
||||
func (p *mdPrinter) text(out *bytes.Buffer, x []Text) {
|
||||
p.raw.Reset()
|
||||
p.rawText(&p.raw, x)
|
||||
line := bytes.TrimSpace(p.raw.Bytes())
|
||||
if len(line) == 0 {
|
||||
return
|
||||
}
|
||||
switch line[0] {
|
||||
case '+', '-', '*', '#':
|
||||
// Escape what would be the start of an unordered list or heading.
|
||||
out.WriteByte('\\')
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||
i := 1
|
||||
for i < len(line) && '0' <= line[i] && line[i] <= '9' {
|
||||
i++
|
||||
}
|
||||
if i < len(line) && (line[i] == '.' || line[i] == ')') {
|
||||
// Escape what would be the start of an ordered list.
|
||||
out.Write(line[:i])
|
||||
out.WriteByte('\\')
|
||||
line = line[i:]
|
||||
}
|
||||
}
|
||||
out.Write(line)
|
||||
}
|
||||
|
||||
// rawText prints the text sequence x to out,
|
||||
// without worrying about escaping characters
|
||||
// that have special meaning at the start of a Markdown line.
|
||||
func (p *mdPrinter) rawText(out *bytes.Buffer, x []Text) {
|
||||
for _, t := range x {
|
||||
switch t := t.(type) {
|
||||
case Plain:
|
||||
p.escape(out, string(t))
|
||||
case Italic:
|
||||
out.WriteString("*")
|
||||
p.escape(out, string(t))
|
||||
out.WriteString("*")
|
||||
case *Link:
|
||||
out.WriteString("[")
|
||||
p.rawText(out, t.Text)
|
||||
out.WriteString("](")
|
||||
out.WriteString(t.URL)
|
||||
out.WriteString(")")
|
||||
case *DocLink:
|
||||
url := p.docLinkURL(t)
|
||||
if url != "" {
|
||||
out.WriteString("[")
|
||||
}
|
||||
p.rawText(out, t.Text)
|
||||
if url != "" {
|
||||
out.WriteString("](")
|
||||
url = strings.ReplaceAll(url, "(", "%28")
|
||||
url = strings.ReplaceAll(url, ")", "%29")
|
||||
out.WriteString(url)
|
||||
out.WriteString(")")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// escape prints s to out as plain text,
|
||||
// escaping special characters to avoid being misinterpreted
|
||||
// as Markdown markup sequences.
|
||||
func (p *mdPrinter) escape(out *bytes.Buffer, s string) {
|
||||
start := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '\n':
|
||||
// Turn all \n into spaces, for a few reasons:
|
||||
// - Avoid introducing paragraph breaks accidentally.
|
||||
// - Avoid the need to reindent after the newline.
|
||||
// - Avoid problems with Markdown renderers treating
|
||||
// every mid-paragraph newline as a <br>.
|
||||
out.WriteString(s[start:i])
|
||||
out.WriteByte(' ')
|
||||
start = i + 1
|
||||
continue
|
||||
case '`', '_', '*', '[', '<', '\\':
|
||||
// Not all of these need to be escaped all the time,
|
||||
// but is valid and easy to do so.
|
||||
// We assume the Markdown is being passed to a
|
||||
// Markdown renderer, not edited by a person,
|
||||
// so it's fine to have escapes that are not strictly
|
||||
// necessary in some cases.
|
||||
out.WriteString(s[start:i])
|
||||
out.WriteByte('\\')
|
||||
out.WriteByte(s[i])
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
out.WriteString(s[start:])
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,288 @@
|
||||
// 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 comment
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A Printer is a doc comment printer.
|
||||
// The fields in the struct can be filled in before calling
|
||||
// any of the printing methods
|
||||
// in order to customize the details of the printing process.
|
||||
type Printer struct {
|
||||
// HeadingLevel is the nesting level used for
|
||||
// HTML and Markdown headings.
|
||||
// If HeadingLevel is zero, it defaults to level 3,
|
||||
// meaning to use <h3> and ###.
|
||||
HeadingLevel int
|
||||
|
||||
// HeadingID is a function that computes the heading ID
|
||||
// (anchor tag) to use for the heading h when generating
|
||||
// HTML and Markdown. If HeadingID returns an empty string,
|
||||
// then the heading ID is omitted.
|
||||
// If HeadingID is nil, h.DefaultID is used.
|
||||
HeadingID func(h *Heading) string
|
||||
|
||||
// DocLinkURL is a function that computes the URL for the given DocLink.
|
||||
// If DocLinkURL is nil, then link.DefaultURL(p.DocLinkBaseURL) is used.
|
||||
DocLinkURL func(link *DocLink) string
|
||||
|
||||
// DocLinkBaseURL is used when DocLinkURL is nil,
|
||||
// passed to [DocLink.DefaultURL] to construct a DocLink's URL.
|
||||
// See that method's documentation for details.
|
||||
DocLinkBaseURL string
|
||||
|
||||
// TextPrefix is a prefix to print at the start of every line
|
||||
// when generating text output using the Text method.
|
||||
TextPrefix string
|
||||
|
||||
// TextCodePrefix is the prefix to print at the start of each
|
||||
// preformatted (code block) line when generating text output,
|
||||
// instead of (not in addition to) TextPrefix.
|
||||
// If TextCodePrefix is the empty string, it defaults to TextPrefix+"\t".
|
||||
TextCodePrefix string
|
||||
|
||||
// TextWidth is the maximum width text line to generate,
|
||||
// measured in Unicode code points,
|
||||
// excluding TextPrefix and the newline character.
|
||||
// If TextWidth is zero, it defaults to 80 minus the number of code points in TextPrefix.
|
||||
// If TextWidth is negative, there is no limit.
|
||||
TextWidth int
|
||||
}
|
||||
|
||||
func (p *Printer) headingLevel() int {
|
||||
if p.HeadingLevel <= 0 {
|
||||
return 3
|
||||
}
|
||||
return p.HeadingLevel
|
||||
}
|
||||
|
||||
func (p *Printer) headingID(h *Heading) string {
|
||||
if p.HeadingID == nil {
|
||||
return h.DefaultID()
|
||||
}
|
||||
return p.HeadingID(h)
|
||||
}
|
||||
|
||||
func (p *Printer) docLinkURL(link *DocLink) string {
|
||||
if p.DocLinkURL != nil {
|
||||
return p.DocLinkURL(link)
|
||||
}
|
||||
return link.DefaultURL(p.DocLinkBaseURL)
|
||||
}
|
||||
|
||||
// DefaultURL constructs and returns the documentation URL for l,
|
||||
// using baseURL as a prefix for links to other packages.
|
||||
//
|
||||
// The possible forms returned by DefaultURL are:
|
||||
// - baseURL/ImportPath, for a link to another package
|
||||
// - baseURL/ImportPath#Name, for a link to a const, func, type, or var in another package
|
||||
// - baseURL/ImportPath#Recv.Name, for a link to a method in another package
|
||||
// - #Name, for a link to a const, func, type, or var in this package
|
||||
// - #Recv.Name, for a link to a method in this package
|
||||
//
|
||||
// If baseURL ends in a trailing slash, then DefaultURL inserts
|
||||
// a slash between ImportPath and # in the anchored forms.
|
||||
// For example, here are some baseURL values and URLs they can generate:
|
||||
//
|
||||
// "/pkg/" → "/pkg/math/#Sqrt"
|
||||
// "/pkg" → "/pkg/math#Sqrt"
|
||||
// "/" → "/math/#Sqrt"
|
||||
// "" → "/math#Sqrt"
|
||||
func (l *DocLink) DefaultURL(baseURL string) string {
|
||||
if l.ImportPath != "" {
|
||||
slash := ""
|
||||
if strings.HasSuffix(baseURL, "/") {
|
||||
slash = "/"
|
||||
} else {
|
||||
baseURL += "/"
|
||||
}
|
||||
switch {
|
||||
case l.Name == "":
|
||||
return baseURL + l.ImportPath + slash
|
||||
case l.Recv != "":
|
||||
return baseURL + l.ImportPath + slash + "#" + l.Recv + "." + l.Name
|
||||
default:
|
||||
return baseURL + l.ImportPath + slash + "#" + l.Name
|
||||
}
|
||||
}
|
||||
if l.Recv != "" {
|
||||
return "#" + l.Recv + "." + l.Name
|
||||
}
|
||||
return "#" + l.Name
|
||||
}
|
||||
|
||||
// DefaultID returns the default anchor ID for the heading h.
|
||||
//
|
||||
// The default anchor ID is constructed by converting every
|
||||
// rune that is not alphanumeric ASCII to an underscore
|
||||
// and then adding the prefix “hdr-”.
|
||||
// For example, if the heading text is “Go Doc Comments”,
|
||||
// the default ID is “hdr-Go_Doc_Comments”.
|
||||
func (h *Heading) DefaultID() string {
|
||||
// Note: The “hdr-” prefix is important to avoid DOM clobbering attacks.
|
||||
// See https://pkg.go.dev/github.com/google/safehtml#Identifier.
|
||||
var out strings.Builder
|
||||
var p textPrinter
|
||||
p.oneLongLine(&out, h.Text)
|
||||
s := strings.TrimSpace(out.String())
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
out.Reset()
|
||||
out.WriteString("hdr-")
|
||||
for _, r := range s {
|
||||
if r < 0x80 && isIdentASCII(byte(r)) {
|
||||
out.WriteByte(byte(r))
|
||||
} else {
|
||||
out.WriteByte('_')
|
||||
}
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
type commentPrinter struct {
|
||||
*Printer
|
||||
}
|
||||
|
||||
// Comment returns the standard Go formatting of the [Doc],
|
||||
// without any comment markers.
|
||||
func (p *Printer) Comment(d *Doc) []byte {
|
||||
cp := &commentPrinter{Printer: p}
|
||||
var out bytes.Buffer
|
||||
for i, x := range d.Content {
|
||||
if i > 0 && blankBefore(x) {
|
||||
out.WriteString("\n")
|
||||
}
|
||||
cp.block(&out, x)
|
||||
}
|
||||
|
||||
// Print one block containing all the link definitions that were used,
|
||||
// and then a second block containing all the unused ones.
|
||||
// This makes it easy to clean up the unused ones: gofmt and
|
||||
// delete the final block. And it's a nice visual signal without
|
||||
// affecting the way the comment formats for users.
|
||||
for i := 0; i < 2; i++ {
|
||||
used := i == 0
|
||||
first := true
|
||||
for _, def := range d.Links {
|
||||
if def.Used == used {
|
||||
if first {
|
||||
out.WriteString("\n")
|
||||
first = false
|
||||
}
|
||||
out.WriteString("[")
|
||||
out.WriteString(def.Text)
|
||||
out.WriteString("]: ")
|
||||
out.WriteString(def.URL)
|
||||
out.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out.Bytes()
|
||||
}
|
||||
|
||||
// blankBefore reports whether the block x requires a blank line before it.
|
||||
// All blocks do, except for Lists that return false from x.BlankBefore().
|
||||
func blankBefore(x Block) bool {
|
||||
if x, ok := x.(*List); ok {
|
||||
return x.BlankBefore()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// block prints the block x to out.
|
||||
func (p *commentPrinter) block(out *bytes.Buffer, x Block) {
|
||||
switch x := x.(type) {
|
||||
default:
|
||||
fmt.Fprintf(out, "?%T", x)
|
||||
|
||||
case *Paragraph:
|
||||
p.text(out, "", x.Text)
|
||||
out.WriteString("\n")
|
||||
|
||||
case *Heading:
|
||||
out.WriteString("# ")
|
||||
p.text(out, "", x.Text)
|
||||
out.WriteString("\n")
|
||||
|
||||
case *Code:
|
||||
md := x.Text
|
||||
for md != "" {
|
||||
var line string
|
||||
line, md, _ = strings.Cut(md, "\n")
|
||||
if line != "" {
|
||||
out.WriteString("\t")
|
||||
out.WriteString(line)
|
||||
}
|
||||
out.WriteString("\n")
|
||||
}
|
||||
|
||||
case *List:
|
||||
loose := x.BlankBetween()
|
||||
for i, item := range x.Items {
|
||||
if i > 0 && loose {
|
||||
out.WriteString("\n")
|
||||
}
|
||||
out.WriteString(" ")
|
||||
if item.Number == "" {
|
||||
out.WriteString(" - ")
|
||||
} else {
|
||||
out.WriteString(item.Number)
|
||||
out.WriteString(". ")
|
||||
}
|
||||
for i, blk := range item.Content {
|
||||
const fourSpace = " "
|
||||
if i > 0 {
|
||||
out.WriteString("\n" + fourSpace)
|
||||
}
|
||||
p.text(out, fourSpace, blk.(*Paragraph).Text)
|
||||
out.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// text prints the text sequence x to out.
|
||||
func (p *commentPrinter) text(out *bytes.Buffer, indent string, x []Text) {
|
||||
for _, t := range x {
|
||||
switch t := t.(type) {
|
||||
case Plain:
|
||||
p.indent(out, indent, string(t))
|
||||
case Italic:
|
||||
p.indent(out, indent, string(t))
|
||||
case *Link:
|
||||
if t.Auto {
|
||||
p.text(out, indent, t.Text)
|
||||
} else {
|
||||
out.WriteString("[")
|
||||
p.text(out, indent, t.Text)
|
||||
out.WriteString("]")
|
||||
}
|
||||
case *DocLink:
|
||||
out.WriteString("[")
|
||||
p.text(out, indent, t.Text)
|
||||
out.WriteString("]")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// indent prints s to out, indenting with the indent string
|
||||
// after each newline in s.
|
||||
func (p *commentPrinter) indent(out *bytes.Buffer, indent, s string) {
|
||||
for s != "" {
|
||||
line, rest, ok := strings.Cut(s, "\n")
|
||||
out.WriteString(line)
|
||||
if ok {
|
||||
out.WriteString("\n")
|
||||
out.WriteString(indent)
|
||||
}
|
||||
s = rest
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// 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.
|
||||
|
||||
// Code generated by 'go generate' DO NOT EDIT.
|
||||
//disabled go:generate ./mkstd.sh
|
||||
|
||||
package comment
|
||||
|
||||
var stdPkgs = []string{
|
||||
"bufio",
|
||||
"bytes",
|
||||
"cmp",
|
||||
"context",
|
||||
"crypto",
|
||||
"embed",
|
||||
"encoding",
|
||||
"errors",
|
||||
"expvar",
|
||||
"flag",
|
||||
"fmt",
|
||||
"hash",
|
||||
"html",
|
||||
"image",
|
||||
"io",
|
||||
"iter",
|
||||
"log",
|
||||
"maps",
|
||||
"math",
|
||||
"mime",
|
||||
"net",
|
||||
"os",
|
||||
"path",
|
||||
"plugin",
|
||||
"reflect",
|
||||
"regexp",
|
||||
"runtime",
|
||||
"slices",
|
||||
"sort",
|
||||
"strconv",
|
||||
"strings",
|
||||
"structs",
|
||||
"sync",
|
||||
"syscall",
|
||||
"testing",
|
||||
"time",
|
||||
"unicode",
|
||||
"unique",
|
||||
"unsafe",
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
// 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 comment
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// 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 (p *Printer) Text(d *Doc) []byte {
|
||||
tp := &textPrinter{
|
||||
Printer: p,
|
||||
prefix: p.TextPrefix,
|
||||
codePrefix: p.TextCodePrefix,
|
||||
width: p.TextWidth,
|
||||
}
|
||||
if tp.codePrefix == "" {
|
||||
tp.codePrefix = p.TextPrefix + "\t"
|
||||
}
|
||||
if tp.width == 0 {
|
||||
tp.width = 80 - utf8.RuneCountInString(tp.prefix)
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
for i, x := range d.Content {
|
||||
if i > 0 && blankBefore(x) {
|
||||
out.WriteString(tp.prefix)
|
||||
writeNL(&out)
|
||||
}
|
||||
tp.block(&out, x)
|
||||
}
|
||||
anyUsed := false
|
||||
for _, def := range d.Links {
|
||||
if def.Used {
|
||||
anyUsed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if anyUsed {
|
||||
writeNL(&out)
|
||||
for _, def := range d.Links {
|
||||
if def.Used {
|
||||
fmt.Fprintf(&out, "[%s]: %s\n", def.Text, def.URL)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out.Bytes()
|
||||
}
|
||||
|
||||
// writeNL calls out.WriteByte('\n')
|
||||
// but first trims trailing spaces on the previous line.
|
||||
func writeNL(out *bytes.Buffer) {
|
||||
// Trim trailing spaces.
|
||||
data := out.Bytes()
|
||||
n := 0
|
||||
for n < len(data) && (data[len(data)-n-1] == ' ' || data[len(data)-n-1] == '\t') {
|
||||
n++
|
||||
}
|
||||
if n > 0 {
|
||||
out.Truncate(len(data) - n)
|
||||
}
|
||||
out.WriteByte('\n')
|
||||
}
|
||||
|
||||
// block prints the block x to out.
|
||||
func (p *textPrinter) block(out *bytes.Buffer, x Block) {
|
||||
switch x := x.(type) {
|
||||
default:
|
||||
fmt.Fprintf(out, "?%T\n", x)
|
||||
|
||||
case *Paragraph:
|
||||
out.WriteString(p.prefix)
|
||||
p.text(out, "", x.Text)
|
||||
|
||||
case *Heading:
|
||||
out.WriteString(p.prefix)
|
||||
out.WriteString("# ")
|
||||
p.text(out, "", x.Text)
|
||||
|
||||
case *Code:
|
||||
text := x.Text
|
||||
for text != "" {
|
||||
var line string
|
||||
line, text, _ = strings.Cut(text, "\n")
|
||||
if line != "" {
|
||||
out.WriteString(p.codePrefix)
|
||||
out.WriteString(line)
|
||||
}
|
||||
writeNL(out)
|
||||
}
|
||||
|
||||
case *List:
|
||||
loose := x.BlankBetween()
|
||||
for i, item := range x.Items {
|
||||
if i > 0 && loose {
|
||||
out.WriteString(p.prefix)
|
||||
writeNL(out)
|
||||
}
|
||||
out.WriteString(p.prefix)
|
||||
out.WriteString(" ")
|
||||
if item.Number == "" {
|
||||
out.WriteString(" - ")
|
||||
} else {
|
||||
out.WriteString(item.Number)
|
||||
out.WriteString(". ")
|
||||
}
|
||||
for i, blk := range item.Content {
|
||||
const fourSpace = " "
|
||||
if i > 0 {
|
||||
writeNL(out)
|
||||
out.WriteString(p.prefix)
|
||||
out.WriteString(fourSpace)
|
||||
}
|
||||
p.text(out, fourSpace, blk.(*Paragraph).Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// text prints the text sequence x to out.
|
||||
func (p *textPrinter) text(out *bytes.Buffer, indent string, x []Text) {
|
||||
p.oneLongLine(&p.long, x)
|
||||
words := strings.Fields(p.long.String())
|
||||
p.long.Reset()
|
||||
|
||||
var seq []int
|
||||
if p.width < 0 || len(words) == 0 {
|
||||
seq = []int{0, len(words)} // one long line
|
||||
} else {
|
||||
seq = wrap(words, p.width-utf8.RuneCountInString(indent))
|
||||
}
|
||||
for i := 0; i+1 < len(seq); i++ {
|
||||
if i > 0 {
|
||||
out.WriteString(p.prefix)
|
||||
out.WriteString(indent)
|
||||
}
|
||||
for j, w := range words[seq[i]:seq[i+1]] {
|
||||
if j > 0 {
|
||||
out.WriteString(" ")
|
||||
}
|
||||
out.WriteString(w)
|
||||
}
|
||||
writeNL(out)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 (p *textPrinter) oneLongLine(out *strings.Builder, x []Text) {
|
||||
for _, t := range x {
|
||||
switch t := t.(type) {
|
||||
case Plain:
|
||||
out.WriteString(string(t))
|
||||
case Italic:
|
||||
out.WriteString(string(t))
|
||||
case *Link:
|
||||
p.oneLongLine(out, t.Text)
|
||||
case *DocLink:
|
||||
p.oneLongLine(out, t.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.60
|
||||
func wrap(words []string, max int) (seq []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.
|
||||
type score struct {
|
||||
hi int64
|
||||
lo int64
|
||||
}
|
||||
add := func(s, t score) score { return score{s.hi + t.hi, s.lo + t.lo} }
|
||||
cmp := func(s, t score) int {
|
||||
switch {
|
||||
case s.hi < t.hi:
|
||||
return -1
|
||||
case s.hi > t.hi:
|
||||
return +1
|
||||
case s.lo < t.lo:
|
||||
return -1
|
||||
case s.lo > t.lo:
|
||||
return +1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// total[j] is the total number of runes
|
||||
// (including separating spaces) in words[:j].
|
||||
total := make([]int, len(words)+1)
|
||||
total[0] = 0
|
||||
for i, s := range words {
|
||||
total[1+i] = total[i] + utf8.RuneCountInString(s) + 1
|
||||
}
|
||||
|
||||
// weight returns weight(i, j).
|
||||
weight := func(i, j int) score {
|
||||
// On the last line, there is zero weight for being too short.
|
||||
n := total[j] - 1 - total[i]
|
||||
if j == len(words) && n <= max {
|
||||
return score{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.)
|
||||
p := wrapPenalty(words[j-1])
|
||||
v := int64(max-n) * int64(max-n)
|
||||
if n > max {
|
||||
return score{v, p}
|
||||
}
|
||||
return score{0, v + p}
|
||||
}
|
||||
|
||||
// The rest of this function is “The Basic Algorithm” from
|
||||
// Hirschberg and Larmore's conference paper,
|
||||
// using the same names as in the paper.
|
||||
f := []score{{0, 0}}
|
||||
g := func(i, j int) score { return add(f[i], weight(i, j)) }
|
||||
|
||||
bridge := func(a, b, c int) bool {
|
||||
k := c + sort.Search(len(words)+1-c, func(k int) bool {
|
||||
k += c
|
||||
return cmp(g(a, k), g(b, k)) > 0
|
||||
})
|
||||
if k > len(words) {
|
||||
return true
|
||||
}
|
||||
return cmp(g(c, k), g(b, k)) <= 0
|
||||
}
|
||||
|
||||
// d is a one-ended deque implemented as a slice.
|
||||
d := make([]int, 1, len(words))
|
||||
d[0] = 0
|
||||
bestleft := make([]int, 1, len(words))
|
||||
bestleft[0] = -1
|
||||
for m := 1; m < len(words); m++ {
|
||||
f = append(f, g(d[0], m))
|
||||
bestleft = append(bestleft, d[0])
|
||||
for len(d) > 1 && cmp(g(d[1], m+1), g(d[0], m+1)) <= 0 {
|
||||
d = d[1:] // “Retire”
|
||||
}
|
||||
for len(d) > 1 && bridge(d[len(d)-2], d[len(d)-1], m) {
|
||||
d = d[:len(d)-1] // “Fire”
|
||||
}
|
||||
if cmp(g(m, len(words)), g(d[len(d)-1], len(words))) < 0 {
|
||||
d = append(d, m) // “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.
|
||||
if len(d) == 2 && cmp(g(d[1], m+1), g(d[0], m+1)) <= 0 {
|
||||
d = d[1:]
|
||||
}
|
||||
}
|
||||
}
|
||||
bestleft = append(bestleft, d[0])
|
||||
|
||||
// Recover least weight sequence from bestleft.
|
||||
n := 1
|
||||
for m := len(words); m > 0; m = bestleft[m] {
|
||||
n++
|
||||
}
|
||||
seq = make([]int, n)
|
||||
for m := len(words); m > 0; m = bestleft[m] {
|
||||
n--
|
||||
seq[n] = m
|
||||
}
|
||||
return seq
|
||||
}
|
||||
|
||||
// wrapPenalty is the penalty for inserting a line break after word s.
|
||||
func wrapPenalty(s string) int64 {
|
||||
switch s[len(s)-1] {
|
||||
case '.', ',', ':', ';':
|
||||
return 0
|
||||
}
|
||||
return 64
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// Copyright 2012 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 format implements standard formatting of Go source.
|
||||
//
|
||||
// Note that formatting of Go source code changes over time, so tools relying on
|
||||
// consistent formatting should execute a specific version of the gofmt binary
|
||||
// instead of using this package. That way, the formatting will be stable, and
|
||||
// the tools won't need to be recompiled each time gofmt changes.
|
||||
//
|
||||
// For example, pre-submit checks that use this package directly would behave
|
||||
// differently depending on what Go version each developer uses, causing the
|
||||
// check to be inherently fragile.
|
||||
package format
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io"
|
||||
|
||||
"mvdan.cc/gofumpt/internal/govendor/go/printer"
|
||||
)
|
||||
|
||||
// Keep these in sync with cmd/gofmt/gofmt.go.
|
||||
const (
|
||||
tabWidth = 8
|
||||
printerMode = printer.UseSpaces | printer.TabIndent | printerNormalizeNumbers
|
||||
|
||||
// printerNormalizeNumbers means to canonicalize number literal prefixes
|
||||
// and exponents while printing. See https://golang.org/doc/go1.13#gofmt.
|
||||
//
|
||||
// This value is defined in mvdan.cc/gofumpt/internal/govendor/go/printer specifically for mvdan.cc/gofumpt/internal/govendor/go/format and cmd/gofmt.
|
||||
printerNormalizeNumbers = 1 << 30
|
||||
)
|
||||
|
||||
var config = printer.Config{Mode: printerMode, Tabwidth: tabWidth}
|
||||
|
||||
const parserMode = parser.ParseComments | parser.SkipObjectResolution
|
||||
|
||||
// Node formats node in canonical gofmt style and writes the result to dst.
|
||||
//
|
||||
// The node type must be *[ast.File], *[printer.CommentedNode], [][ast.Decl],
|
||||
// [][ast.Stmt], or assignment-compatible to [ast.Expr], [ast.Decl], [ast.Spec],
|
||||
// or [ast.Stmt]. Node does not modify node. Imports are not sorted for
|
||||
// nodes representing partial source files (for instance, if the node is
|
||||
// not an *[ast.File] or a *[printer.CommentedNode] not wrapping an *[ast.File]).
|
||||
//
|
||||
// The function may return early (before the entire result is written)
|
||||
// and return a formatting error, for instance due to an incorrect AST.
|
||||
func Node(dst io.Writer, fset *token.FileSet, node any) error {
|
||||
// Determine if we have a complete source file (file != nil).
|
||||
var file *ast.File
|
||||
var cnode *printer.CommentedNode
|
||||
switch n := node.(type) {
|
||||
case *ast.File:
|
||||
file = n
|
||||
case *printer.CommentedNode:
|
||||
if f, ok := n.Node.(*ast.File); ok {
|
||||
file = f
|
||||
cnode = n
|
||||
}
|
||||
}
|
||||
|
||||
// Sort imports if necessary.
|
||||
if file != nil && hasUnsortedImports(file) {
|
||||
// Make a copy of the AST because ast.SortImports is destructive.
|
||||
// TODO(gri) Do this more efficiently.
|
||||
var buf bytes.Buffer
|
||||
err := config.Fprint(&buf, fset, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err = parser.ParseFile(fset, "", buf.Bytes(), parserMode)
|
||||
if err != nil {
|
||||
// We should never get here. If we do, provide good diagnostic.
|
||||
return fmt.Errorf("format.Node internal error (%s)", err)
|
||||
}
|
||||
ast.SortImports(fset, file)
|
||||
|
||||
// Use new file with sorted imports.
|
||||
node = file
|
||||
if cnode != nil {
|
||||
node = &printer.CommentedNode{Node: file, Comments: cnode.Comments}
|
||||
}
|
||||
}
|
||||
|
||||
return config.Fprint(dst, fset, node)
|
||||
}
|
||||
|
||||
// Source formats src in canonical gofmt style and returns the result
|
||||
// or an (I/O or syntax) error. src is expected to be a syntactically
|
||||
// correct Go source file, or a list of Go declarations or statements.
|
||||
//
|
||||
// If src is a partial source file, the leading and trailing space of src
|
||||
// is applied to the result (such that it has the same leading and trailing
|
||||
// space as src), and the result is indented by the same amount as the first
|
||||
// line of src containing code. Imports are not sorted for partial source files.
|
||||
func Source(src []byte) ([]byte, error) {
|
||||
fset := token.NewFileSet()
|
||||
file, sourceAdj, indentAdj, err := parse(fset, "", src, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if sourceAdj == nil {
|
||||
// Complete source file.
|
||||
// TODO(gri) consider doing this always.
|
||||
ast.SortImports(fset, file)
|
||||
}
|
||||
|
||||
return format(fset, file, sourceAdj, indentAdj, src, config)
|
||||
}
|
||||
|
||||
func hasUnsortedImports(file *ast.File) bool {
|
||||
for _, d := range file.Decls {
|
||||
d, ok := d.(*ast.GenDecl)
|
||||
if !ok || d.Tok != token.IMPORT {
|
||||
// Not an import declaration, so we're done.
|
||||
// Imports are always first.
|
||||
return false
|
||||
}
|
||||
if d.Lparen.IsValid() {
|
||||
// For now assume all grouped imports are unsorted.
|
||||
// TODO(gri) Should check if they are sorted already.
|
||||
return true
|
||||
}
|
||||
// Ungrouped imports are sorted by default.
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
// Copyright 2015 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.
|
||||
|
||||
// TODO(gri): This file and the file src/cmd/gofmt/internal.go are
|
||||
// the same (but for this comment and the package name). Do not modify
|
||||
// one without the other. Determine if we can factor out functionality
|
||||
// in a public API. See also #11844 for context.
|
||||
|
||||
package format
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"strings"
|
||||
|
||||
"mvdan.cc/gofumpt/internal/govendor/go/printer"
|
||||
)
|
||||
|
||||
// parse parses src, which was read from the named file,
|
||||
// as a Go source file, declaration, or statement list.
|
||||
func parse(fset *token.FileSet, filename string, src []byte, fragmentOk bool) (
|
||||
file *ast.File,
|
||||
sourceAdj func(src []byte, indent int) []byte,
|
||||
indentAdj int,
|
||||
err error,
|
||||
) {
|
||||
// Try as whole source file.
|
||||
file, err = parser.ParseFile(fset, filename, src, parserMode)
|
||||
// If there's no error, return. If the error is that the source file didn't begin with a
|
||||
// package line and source fragments are ok, fall through to
|
||||
// try as a source fragment. Stop and return on any other error.
|
||||
if err == nil || !fragmentOk || !strings.Contains(err.Error(), "expected 'package'") {
|
||||
return
|
||||
}
|
||||
|
||||
// If this is a declaration list, make it a source file
|
||||
// by inserting a package clause.
|
||||
// Insert using a ';', not a newline, so that the line numbers
|
||||
// in psrc match the ones in src.
|
||||
psrc := append([]byte("package p;"), src...)
|
||||
file, err = parser.ParseFile(fset, filename, psrc, parserMode)
|
||||
if err == nil {
|
||||
sourceAdj = func(src []byte, indent int) []byte {
|
||||
// Remove the package clause.
|
||||
// Gofmt has turned the ';' into a '\n'.
|
||||
src = src[indent+len("package p\n"):]
|
||||
return bytes.TrimSpace(src)
|
||||
}
|
||||
return
|
||||
}
|
||||
// If the error is that the source file didn't begin with a
|
||||
// declaration, fall through to try as a statement list.
|
||||
// Stop and return on any other error.
|
||||
if !strings.Contains(err.Error(), "expected declaration") {
|
||||
return
|
||||
}
|
||||
|
||||
// If this is a statement list, make it a source file
|
||||
// by inserting a package clause and turning the list
|
||||
// into a function body. This handles expressions too.
|
||||
// Insert using a ';', not a newline, so that the line numbers
|
||||
// in fsrc match the ones in src. Add an extra '\n' before the '}'
|
||||
// to make sure comments are flushed before the '}'.
|
||||
fsrc := append(append([]byte("package p; func _() {"), src...), '\n', '\n', '}')
|
||||
file, err = parser.ParseFile(fset, filename, fsrc, parserMode)
|
||||
if err == nil {
|
||||
sourceAdj = func(src []byte, indent int) []byte {
|
||||
// Cap adjusted indent to zero.
|
||||
if indent < 0 {
|
||||
indent = 0
|
||||
}
|
||||
// Remove the wrapping.
|
||||
// Gofmt has turned the "; " into a "\n\n".
|
||||
// There will be two non-blank lines with indent, hence 2*indent.
|
||||
src = src[2*indent+len("package p\n\nfunc _() {"):]
|
||||
// Remove only the "}\n" suffix: remaining whitespaces will be trimmed anyway
|
||||
src = src[:len(src)-len("}\n")]
|
||||
return bytes.TrimSpace(src)
|
||||
}
|
||||
// Gofmt has also indented the function body one level.
|
||||
// Adjust that with indentAdj.
|
||||
indentAdj = -1
|
||||
}
|
||||
|
||||
// Succeeded, or out of options.
|
||||
return
|
||||
}
|
||||
|
||||
// format formats the given package file originally obtained from src
|
||||
// and adjusts the result based on the original source via sourceAdj
|
||||
// and indentAdj.
|
||||
func format(
|
||||
fset *token.FileSet,
|
||||
file *ast.File,
|
||||
sourceAdj func(src []byte, indent int) []byte,
|
||||
indentAdj int,
|
||||
src []byte,
|
||||
cfg printer.Config,
|
||||
) ([]byte, error) {
|
||||
if sourceAdj == nil {
|
||||
// Complete source file.
|
||||
var buf bytes.Buffer
|
||||
err := cfg.Fprint(&buf, fset, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// Partial source file.
|
||||
// Determine and prepend leading space.
|
||||
i, j := 0, 0
|
||||
for j < len(src) && isSpace(src[j]) {
|
||||
if src[j] == '\n' {
|
||||
i = j + 1 // byte offset of last line in leading space
|
||||
}
|
||||
j++
|
||||
}
|
||||
var res []byte
|
||||
res = append(res, src[:i]...)
|
||||
|
||||
// Determine and prepend indentation of first code line.
|
||||
// Spaces are ignored unless there are no tabs,
|
||||
// in which case spaces count as one tab.
|
||||
indent := 0
|
||||
hasSpace := false
|
||||
for _, b := range src[i:j] {
|
||||
switch b {
|
||||
case ' ':
|
||||
hasSpace = true
|
||||
case '\t':
|
||||
indent++
|
||||
}
|
||||
}
|
||||
if indent == 0 && hasSpace {
|
||||
indent = 1
|
||||
}
|
||||
for i := 0; i < indent; i++ {
|
||||
res = append(res, '\t')
|
||||
}
|
||||
|
||||
// Format the source.
|
||||
// Write it without any leading and trailing space.
|
||||
cfg.Indent = indent + indentAdj
|
||||
var buf bytes.Buffer
|
||||
err := cfg.Fprint(&buf, fset, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := sourceAdj(buf.Bytes(), cfg.Indent)
|
||||
|
||||
// If the adjusted output is empty, the source
|
||||
// was empty but (possibly) for white space.
|
||||
// The result is the incoming source.
|
||||
if len(out) == 0 {
|
||||
return src, nil
|
||||
}
|
||||
|
||||
// Otherwise, append output to leading space.
|
||||
res = append(res, out...)
|
||||
|
||||
// Determine and append trailing space.
|
||||
i = len(src)
|
||||
for i > 0 && isSpace(src[i-1]) {
|
||||
i--
|
||||
}
|
||||
return append(res, src[i:]...), nil
|
||||
}
|
||||
|
||||
// isSpace reports whether the byte is a space character.
|
||||
// isSpace defines a space as being among the following bytes: ' ', '\t', '\n' and '\r'.
|
||||
func isSpace(b byte) bool {
|
||||
return b == ' ' || b == '\t' || b == '\n' || b == '\r'
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// 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 printer
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"strings"
|
||||
|
||||
"mvdan.cc/gofumpt/internal/govendor/go/doc/comment"
|
||||
)
|
||||
|
||||
// formatDocComment reformats the doc comment list,
|
||||
// returning the canonical formatting.
|
||||
func formatDocComment(list []*ast.Comment) []*ast.Comment {
|
||||
// Extract comment text (removing comment markers).
|
||||
var kind, text string
|
||||
var directives []*ast.Comment
|
||||
if len(list) == 1 && strings.HasPrefix(list[0].Text, "/*") {
|
||||
kind = "/*"
|
||||
text = list[0].Text
|
||||
if !strings.Contains(text, "\n") || allStars(text) {
|
||||
// Single-line /* .. */ comment in doc comment position,
|
||||
// or multiline old-style comment like
|
||||
// /*
|
||||
// * Comment
|
||||
// * text here.
|
||||
// */
|
||||
// Should not happen, since it will not work well as a
|
||||
// doc comment, but if it does, just ignore:
|
||||
// reformatting it will only make the situation worse.
|
||||
return list
|
||||
}
|
||||
text = text[2 : len(text)-2] // cut /* and */
|
||||
} else if strings.HasPrefix(list[0].Text, "//") {
|
||||
kind = "//"
|
||||
var b strings.Builder
|
||||
for _, c := range list {
|
||||
after, found := strings.CutPrefix(c.Text, "//")
|
||||
if !found {
|
||||
return list
|
||||
}
|
||||
// Accumulate //go:build etc lines separately.
|
||||
if isDirective(after) {
|
||||
directives = append(directives, c)
|
||||
continue
|
||||
}
|
||||
b.WriteString(strings.TrimPrefix(after, " "))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
text = b.String()
|
||||
} else {
|
||||
// Not sure what this is, so leave alone.
|
||||
return list
|
||||
}
|
||||
|
||||
if text == "" {
|
||||
return list
|
||||
}
|
||||
|
||||
// Parse comment and reformat as text.
|
||||
var p comment.Parser
|
||||
d := p.Parse(text)
|
||||
|
||||
var pr comment.Printer
|
||||
text = string(pr.Comment(d))
|
||||
|
||||
// For /* */ comment, return one big comment with text inside.
|
||||
slash := list[0].Slash
|
||||
if kind == "/*" {
|
||||
c := &ast.Comment{
|
||||
Slash: slash,
|
||||
Text: "/*\n" + text + "*/",
|
||||
}
|
||||
return []*ast.Comment{c}
|
||||
}
|
||||
|
||||
// For // comment, return sequence of // lines.
|
||||
var out []*ast.Comment
|
||||
for text != "" {
|
||||
var line string
|
||||
line, text, _ = strings.Cut(text, "\n")
|
||||
if line == "" {
|
||||
line = "//"
|
||||
} else if strings.HasPrefix(line, "\t") {
|
||||
line = "//" + line
|
||||
} else {
|
||||
line = "// " + line
|
||||
}
|
||||
out = append(out, &ast.Comment{
|
||||
Slash: slash,
|
||||
Text: line,
|
||||
})
|
||||
}
|
||||
if len(directives) > 0 {
|
||||
out = append(out, &ast.Comment{
|
||||
Slash: slash,
|
||||
Text: "//",
|
||||
})
|
||||
for _, c := range directives {
|
||||
out = append(out, &ast.Comment{
|
||||
Slash: slash,
|
||||
Text: c.Text,
|
||||
})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// isDirective reports whether c is a comment directive.
|
||||
// See go.dev/issue/37974.
|
||||
// This code is also in go/ast.
|
||||
func isDirective(c string) bool {
|
||||
// "//line " is a line directive.
|
||||
// "//extern " is for gccgo.
|
||||
// "//export " is for cgo.
|
||||
// (The // has been removed.)
|
||||
if strings.HasPrefix(c, "line ") || strings.HasPrefix(c, "extern ") || strings.HasPrefix(c, "export ") {
|
||||
return true
|
||||
}
|
||||
|
||||
// "//[a-z0-9]+:[a-z0-9]"
|
||||
// (The // has been removed.)
|
||||
colon := strings.Index(c, ":")
|
||||
if colon <= 0 || colon+1 >= len(c) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i <= colon+1; i++ {
|
||||
if i == colon {
|
||||
continue
|
||||
}
|
||||
b := c[i]
|
||||
if !('a' <= b && b <= 'z' || '0' <= b && b <= '9') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// allStars reports whether text is the interior of an
|
||||
// old-style /* */ comment with a star at the start of each line.
|
||||
func allStars(text string) bool {
|
||||
for i := 0; i < len(text); i++ {
|
||||
if text[i] == '\n' {
|
||||
j := i + 1
|
||||
for j < len(text) && (text[j] == ' ' || text[j] == '\t') {
|
||||
j++
|
||||
}
|
||||
if j < len(text) && text[j] != '*' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// Copyright 2020 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 printer
|
||||
|
||||
import (
|
||||
"go/build/constraint"
|
||||
"slices"
|
||||
"text/tabwriter"
|
||||
)
|
||||
|
||||
func (p *printer) fixGoBuildLines() {
|
||||
if len(p.goBuild)+len(p.plusBuild) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Find latest possible placement of //go:build and // +build comments.
|
||||
// That's just after the last blank line before we find a non-comment.
|
||||
// (We'll add another blank line after our comment block.)
|
||||
// When we start dropping // +build comments, we can skip over /* */ comments too.
|
||||
// Note that we are processing tabwriter input, so every comment
|
||||
// begins and ends with a tabwriter.Escape byte.
|
||||
// And some newlines have turned into \f bytes.
|
||||
insert := 0
|
||||
for pos := 0; ; {
|
||||
// Skip leading space at beginning of line.
|
||||
blank := true
|
||||
for pos < len(p.output) && (p.output[pos] == ' ' || p.output[pos] == '\t') {
|
||||
pos++
|
||||
}
|
||||
// Skip over // comment if any.
|
||||
if pos+3 < len(p.output) && p.output[pos] == tabwriter.Escape && p.output[pos+1] == '/' && p.output[pos+2] == '/' {
|
||||
blank = false
|
||||
for pos < len(p.output) && !isNL(p.output[pos]) {
|
||||
pos++
|
||||
}
|
||||
}
|
||||
// Skip over \n at end of line.
|
||||
if pos >= len(p.output) || !isNL(p.output[pos]) {
|
||||
break
|
||||
}
|
||||
pos++
|
||||
|
||||
if blank {
|
||||
insert = pos
|
||||
}
|
||||
}
|
||||
|
||||
// If there is a //go:build comment before the place we identified,
|
||||
// use that point instead. (Earlier in the file is always fine.)
|
||||
if len(p.goBuild) > 0 && p.goBuild[0] < insert {
|
||||
insert = p.goBuild[0]
|
||||
} else if len(p.plusBuild) > 0 && p.plusBuild[0] < insert {
|
||||
insert = p.plusBuild[0]
|
||||
}
|
||||
|
||||
var x constraint.Expr
|
||||
switch len(p.goBuild) {
|
||||
case 0:
|
||||
// Synthesize //go:build expression from // +build lines.
|
||||
for _, pos := range p.plusBuild {
|
||||
y, err := constraint.Parse(p.commentTextAt(pos))
|
||||
if err != nil {
|
||||
x = nil
|
||||
break
|
||||
}
|
||||
if x == nil {
|
||||
x = y
|
||||
} else {
|
||||
x = &constraint.AndExpr{X: x, Y: y}
|
||||
}
|
||||
}
|
||||
case 1:
|
||||
// Parse //go:build expression.
|
||||
x, _ = constraint.Parse(p.commentTextAt(p.goBuild[0]))
|
||||
}
|
||||
|
||||
var block []byte
|
||||
if x == nil {
|
||||
// Don't have a valid //go:build expression to treat as truth.
|
||||
// Bring all the lines together but leave them alone.
|
||||
// Note that these are already tabwriter-escaped.
|
||||
for _, pos := range p.goBuild {
|
||||
block = append(block, p.lineAt(pos)...)
|
||||
}
|
||||
for _, pos := range p.plusBuild {
|
||||
block = append(block, p.lineAt(pos)...)
|
||||
}
|
||||
} else {
|
||||
block = append(block, tabwriter.Escape)
|
||||
block = append(block, "//go:build "...)
|
||||
block = append(block, x.String()...)
|
||||
block = append(block, tabwriter.Escape, '\n')
|
||||
if len(p.plusBuild) > 0 {
|
||||
lines, err := constraint.PlusBuildLines(x)
|
||||
if err != nil {
|
||||
lines = []string{"// +build error: " + err.Error()}
|
||||
}
|
||||
for _, line := range lines {
|
||||
block = append(block, tabwriter.Escape)
|
||||
block = append(block, line...)
|
||||
block = append(block, tabwriter.Escape, '\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
block = append(block, '\n')
|
||||
|
||||
// Build sorted list of lines to delete from remainder of output.
|
||||
toDelete := append(p.goBuild, p.plusBuild...)
|
||||
slices.Sort(toDelete)
|
||||
|
||||
// Collect output after insertion point, with lines deleted, into after.
|
||||
var after []byte
|
||||
start := insert
|
||||
for _, end := range toDelete {
|
||||
if end < start {
|
||||
continue
|
||||
}
|
||||
after = appendLines(after, p.output[start:end])
|
||||
start = end + len(p.lineAt(end))
|
||||
}
|
||||
after = appendLines(after, p.output[start:])
|
||||
if n := len(after); n >= 2 && isNL(after[n-1]) && isNL(after[n-2]) {
|
||||
after = after[:n-1]
|
||||
}
|
||||
|
||||
p.output = p.output[:insert]
|
||||
p.output = append(p.output, block...)
|
||||
p.output = append(p.output, after...)
|
||||
}
|
||||
|
||||
// appendLines is like append(x, y...)
|
||||
// but it avoids creating doubled blank lines,
|
||||
// which would not be gofmt-standard output.
|
||||
// It assumes that only whole blocks of lines are being appended,
|
||||
// not line fragments.
|
||||
func appendLines(x, y []byte) []byte {
|
||||
if len(y) > 0 && isNL(y[0]) && // y starts in blank line
|
||||
(len(x) == 0 || len(x) >= 2 && isNL(x[len(x)-1]) && isNL(x[len(x)-2])) { // x is empty or ends in blank line
|
||||
y = y[1:] // delete y's leading blank line
|
||||
}
|
||||
return append(x, y...)
|
||||
}
|
||||
|
||||
func (p *printer) lineAt(start int) []byte {
|
||||
pos := start
|
||||
for pos < len(p.output) && !isNL(p.output[pos]) {
|
||||
pos++
|
||||
}
|
||||
if pos < len(p.output) {
|
||||
pos++
|
||||
}
|
||||
return p.output[start:pos]
|
||||
}
|
||||
|
||||
func (p *printer) commentTextAt(start int) string {
|
||||
if start < len(p.output) && p.output[start] == tabwriter.Escape {
|
||||
start++
|
||||
}
|
||||
pos := start
|
||||
for pos < len(p.output) && p.output[pos] != tabwriter.Escape && !isNL(p.output[pos]) {
|
||||
pos++
|
||||
}
|
||||
return string(p.output[start:pos])
|
||||
}
|
||||
|
||||
func isNL(b byte) bool {
|
||||
return b == '\n' || b == '\f'
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
go1.23.0
|
||||
Reference in New Issue
Block a user