Forráskód Böngészése

cmd/elvmdfmt: Split Markdown formatting into own command, supporting -w and -d.

Also change the behavior of the rendering command (elvmd) to still output HTML
when given -trace.
Qi Xiao 1 éve
szülő
commit
b901b49f12
7 módosított fájl, 381 hozzáadás és 30 törlés
  1. 2 1
      .codecov.yml
  2. 4 3
      README.md
  3. 27 21
      cmd/elvmd/main.go
  4. 55 0
      cmd/elvmdfmt/main.go
  5. 27 0
      pkg/diff/LICENSE
  6. 261 0
      pkg/diff/diff.go
  7. 5 5
      pkg/md/trace.go

+ 2 - 1
.codecov.yml

@@ -20,5 +20,6 @@ ignore:
   - "pkg/getopt/zstring.go"
   - "pkg/md/zstring.go"
   - "pkg/parse/zstring.go"
-  # Exclude the copied rpc package.
+  # Exclude the copied diff and rpc packages.
+  - "pkg/diff"
   - "pkg/rpc"

+ 4 - 3
README.md

@@ -41,9 +41,10 @@ The source for the documentation is in the
 All source files use the BSD 2-clause license (see [LICENSE](LICENSE)), except
 for the following:
 
--   Files in [pkg/rpc](pkg/rpc) are released under the BSD 3-clause license,
-    since they are copied from [Go's source code](https://github.com/golang/go).
-    See [pkg/rpc/LICENSE](pkg/rpc/LICENSE).
+-   Files in [pkg/diff](pkg/diff) and [pkg/rpc](pkg/rpc) are released under the
+    BSD 3-clause license, since they are copied from
+    [Go's source code](https://github.com/golang/go). See
+    [pkg/diff/LICENSE](pkg/diff/LICENSE) and [pkg/rpc/LICENSE](pkg/rpc/LICENSE).
 
 -   Files in [pkg/persistent](pkg/persistent) and its subdirectories are
     released under EPL 1.0, since they are partially derived from

+ 27 - 21
cmd/elvmd/main.go

@@ -4,35 +4,41 @@ import (
 	"flag"
 	"fmt"
 	"io"
-	"log"
 	"os"
 
 	"src.elv.sh/pkg/md"
 )
 
+var (
+	trace = flag.Bool("trace", false, "write internal parsing results")
+)
+
 func main() {
-	var (
-		format = flag.Bool("fmt", false, "format Markdown")
-		trace  = flag.Bool("trace", false, "trace internal output by parser")
-	)
 	flag.Parse()
-	if *format && *trace {
-		fmt.Fprintln(os.Stderr, "-fmt and -trace are mutually exclusive")
-		os.Exit(1)
-	}
 
-	text, err := io.ReadAll(os.Stdin)
-	if err != nil {
-		log.Fatal(err)
+	files := flag.Args()
+	if len(files) == 0 {
+		text, err := io.ReadAll(os.Stdin)
+		if err != nil {
+			fmt.Fprintln(os.Stderr, "read stdin:", err)
+			os.Exit(2)
+		}
+		render(string(text))
+		return
 	}
-	var codec md.StringerCodec
-	switch {
-	case *format:
-		codec = &md.FmtCodec{}
-	case *trace:
-		codec = &md.TraceCodec{}
-	default:
-		codec = &md.HTMLCodec{}
+	for _, file := range files {
+		text, err := os.ReadFile(file)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "read %s: %v\n", file, err)
+			os.Exit(2)
+		}
+		render(string(text))
+	}
+}
+
+func render(markdown string) {
+	fmt.Print(md.RenderString(markdown, &md.HTMLCodec{}))
+	if *trace {
+		fmt.Print(md.RenderString(markdown, &md.TraceCodec{}))
 	}
-	fmt.Print(md.RenderString(string(text), codec))
 }

+ 55 - 0
cmd/elvmdfmt/main.go

@@ -0,0 +1,55 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"io"
+	"os"
+
+	"src.elv.sh/pkg/diff"
+	"src.elv.sh/pkg/md"
+)
+
+var (
+	overwrite = flag.Bool("w", false, "write result to source file (requires -fmt)")
+	showDiff  = flag.Bool("d", false, "show diff")
+)
+
+func main() {
+	flag.Parse()
+
+	files := flag.Args()
+	if len(files) == 0 {
+		text, err := io.ReadAll(os.Stdin)
+		if err != nil {
+			fmt.Fprintln(os.Stderr, "read stdin:", err)
+			os.Exit(2)
+		}
+		fmt.Print(format(string(text)))
+		return
+	}
+	for _, file := range files {
+		text, err := os.ReadFile(file)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "read %s: %v\n", file, err)
+			os.Exit(2)
+		}
+		result := format(string(text))
+		if *overwrite {
+			err := os.WriteFile(file, []byte(result), 0644)
+			if err != nil {
+				fmt.Fprintf(os.Stderr, "write %s: %v\n", file, err)
+				os.Exit(2)
+			}
+		} else if !*showDiff {
+			fmt.Print(result)
+		}
+		if *showDiff {
+			os.Stdout.Write(diff.Diff(file+".orig", text, file, []byte(result)))
+		}
+	}
+}
+
+func format(original string) string {
+	return md.RenderString(original, &md.FmtCodec{})
+}

+ 27 - 0
pkg/diff/LICENSE

@@ -0,0 +1,27 @@
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 261 - 0
pkg/diff/diff.go

@@ -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 possible,
+		// establishing that x[start.x:end.x] == y[start.y:end.y].
+		// Note that on the first (or last) iteration we may (or definitey 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
+}

+ 5 - 5
pkg/md/trace.go

@@ -9,9 +9,6 @@ import (
 type TraceCodec struct{ strings.Builder }
 
 func (c *TraceCodec) Do(op Op) {
-	if c.Len() > 0 {
-		c.WriteByte('\n')
-	}
 	c.WriteString(op.Type.String())
 	if op.Number != 0 {
 		fmt.Fprintf(c, " Number=%d", op.Number)
@@ -22,12 +19,14 @@ func (c *TraceCodec) Do(op Op) {
 	if op.MissingCloser {
 		fmt.Fprintf(c, " MissingCloser")
 	}
+	c.WriteByte('\n')
 	for _, line := range op.Lines {
-		c.WriteString("\n  ")
+		c.WriteString("  ")
 		c.WriteString(line)
+		c.WriteByte('\n')
 	}
 	for _, inlineOp := range op.Content {
-		c.WriteString("\n  ")
+		c.WriteString("  ")
 		c.WriteString(inlineOp.Type.String())
 		if inlineOp.Text != "" {
 			fmt.Fprintf(c, " Text=%q", inlineOp.Text)
@@ -38,5 +37,6 @@ func (c *TraceCodec) Do(op Op) {
 		if inlineOp.Alt != "" {
 			fmt.Fprintf(c, " Alt=%q", inlineOp.Alt)
 		}
+		c.WriteString("\n")
 	}
 }