github.com/graybobo/golang.org-package-offline-cache@v0.0.0-20200626051047-6608995c132f/x/tools/present/code.go (about)

     1  // Copyright 2012 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package present
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"fmt"
    11  	"html/template"
    12  	"path/filepath"
    13  	"regexp"
    14  	"strconv"
    15  	"strings"
    16  )
    17  
    18  // Is the playground available?
    19  var PlayEnabled = false
    20  
    21  // TOOD(adg): replace the PlayEnabled flag with something less spaghetti-like.
    22  // Instead this will probably be determined by a template execution Context
    23  // value that contains various global metadata required when rendering
    24  // templates.
    25  
    26  func init() {
    27  	Register("code", parseCode)
    28  	Register("play", parseCode)
    29  }
    30  
    31  type Code struct {
    32  	Text     template.HTML
    33  	Play     bool   // runnable code
    34  	FileName string // file name
    35  	Ext      string // file extension
    36  	Raw      []byte // content of the file
    37  }
    38  
    39  func (c Code) TemplateName() string { return "code" }
    40  
    41  // The input line is a .code or .play entry with a file name and an optional HLfoo marker on the end.
    42  // Anything between the file and HL (if any) is an address expression, which we treat as a string here.
    43  // We pick off the HL first, for easy parsing.
    44  var (
    45  	highlightRE = regexp.MustCompile(`\s+HL([a-zA-Z0-9_]+)?$`)
    46  	hlCommentRE = regexp.MustCompile(`(.+) // HL(.*)$`)
    47  	codeRE      = regexp.MustCompile(`\.(code|play)\s+((?:(?:-edit|-numbers)\s+)*)([^\s]+)(?:\s+(.*))?$`)
    48  )
    49  
    50  // parseCode parses a code present directive. Its syntax:
    51  //   .code [-numbers] [-edit] <filename> [address] [highlight]
    52  // The directive may also be ".play" if the snippet is executable.
    53  func parseCode(ctx *Context, sourceFile string, sourceLine int, cmd string) (Elem, error) {
    54  	cmd = strings.TrimSpace(cmd)
    55  
    56  	// Pull off the HL, if any, from the end of the input line.
    57  	highlight := ""
    58  	if hl := highlightRE.FindStringSubmatchIndex(cmd); len(hl) == 4 {
    59  		highlight = cmd[hl[2]:hl[3]]
    60  		cmd = cmd[:hl[2]-2]
    61  	}
    62  
    63  	// Parse the remaining command line.
    64  	// Arguments:
    65  	// args[0]: whole match
    66  	// args[1]:  .code/.play
    67  	// args[2]: flags ("-edit -numbers")
    68  	// args[3]: file name
    69  	// args[4]: optional address
    70  	args := codeRE.FindStringSubmatch(cmd)
    71  	if len(args) != 5 {
    72  		return nil, fmt.Errorf("%s:%d: syntax error for .code/.play invocation", sourceFile, sourceLine)
    73  	}
    74  	command, flags, file, addr := args[1], args[2], args[3], strings.TrimSpace(args[4])
    75  	play := command == "play" && PlayEnabled
    76  
    77  	// Read in code file and (optionally) match address.
    78  	filename := filepath.Join(filepath.Dir(sourceFile), file)
    79  	textBytes, err := ctx.ReadFile(filename)
    80  	if err != nil {
    81  		return nil, fmt.Errorf("%s:%d: %v", sourceFile, sourceLine, err)
    82  	}
    83  	lo, hi, err := addrToByteRange(addr, 0, textBytes)
    84  	if err != nil {
    85  		return nil, fmt.Errorf("%s:%d: %v", sourceFile, sourceLine, err)
    86  	}
    87  
    88  	// Acme pattern matches can stop mid-line,
    89  	// so run to end of line in both directions if not at line start/end.
    90  	for lo > 0 && textBytes[lo-1] != '\n' {
    91  		lo--
    92  	}
    93  	if hi > 0 {
    94  		for hi < len(textBytes) && textBytes[hi-1] != '\n' {
    95  			hi++
    96  		}
    97  	}
    98  
    99  	lines := codeLines(textBytes, lo, hi)
   100  
   101  	data := &codeTemplateData{
   102  		Lines:   formatLines(lines, highlight),
   103  		Edit:    strings.Contains(flags, "-edit"),
   104  		Numbers: strings.Contains(flags, "-numbers"),
   105  	}
   106  
   107  	// Include before and after in a hidden span for playground code.
   108  	if play {
   109  		data.Prefix = textBytes[:lo]
   110  		data.Suffix = textBytes[hi:]
   111  	}
   112  
   113  	var buf bytes.Buffer
   114  	if err := codeTemplate.Execute(&buf, data); err != nil {
   115  		return nil, err
   116  	}
   117  	return Code{
   118  		Text:     template.HTML(buf.String()),
   119  		Play:     play,
   120  		FileName: filepath.Base(filename),
   121  		Ext:      filepath.Ext(filename),
   122  		Raw:      rawCode(lines),
   123  	}, nil
   124  }
   125  
   126  // formatLines returns a new slice of codeLine with the given lines
   127  // replacing tabs with spaces and adding highlighting where needed.
   128  func formatLines(lines []codeLine, highlight string) []codeLine {
   129  	formatted := make([]codeLine, len(lines))
   130  	for i, line := range lines {
   131  		// Replace tabs with spaces, which work better in HTML.
   132  		line.L = strings.Replace(line.L, "\t", "    ", -1)
   133  
   134  		// Highlight lines that end with "// HL[highlight]"
   135  		// and strip the magic comment.
   136  		if m := hlCommentRE.FindStringSubmatch(line.L); m != nil {
   137  			line.L = m[1]
   138  			line.HL = m[2] == highlight
   139  		}
   140  
   141  		formatted[i] = line
   142  	}
   143  	return formatted
   144  }
   145  
   146  // rawCode returns the code represented by the given codeLines without any kind
   147  // of formatting.
   148  func rawCode(lines []codeLine) []byte {
   149  	b := new(bytes.Buffer)
   150  	for _, line := range lines {
   151  		b.WriteString(line.L)
   152  		b.WriteByte('\n')
   153  	}
   154  	return b.Bytes()
   155  }
   156  
   157  type codeTemplateData struct {
   158  	Lines          []codeLine
   159  	Prefix, Suffix []byte
   160  	Edit, Numbers  bool
   161  }
   162  
   163  var leadingSpaceRE = regexp.MustCompile(`^[ \t]*`)
   164  
   165  var codeTemplate = template.Must(template.New("code").Funcs(template.FuncMap{
   166  	"trimSpace":    strings.TrimSpace,
   167  	"leadingSpace": leadingSpaceRE.FindString,
   168  }).Parse(codeTemplateHTML))
   169  
   170  const codeTemplateHTML = `
   171  {{with .Prefix}}<pre style="display: none"><span>{{printf "%s" .}}</span></pre>{{end}}
   172  
   173  <pre{{if .Edit}} contenteditable="true" spellcheck="false"{{end}}{{if .Numbers}} class="numbers"{{end}}>{{/*
   174  	*/}}{{range .Lines}}<span num="{{.N}}">{{/*
   175  	*/}}{{if .HL}}{{leadingSpace .L}}<b>{{trimSpace .L}}</b>{{/*
   176  	*/}}{{else}}{{.L}}{{end}}{{/*
   177  */}}</span>
   178  {{end}}</pre>
   179  
   180  {{with .Suffix}}<pre style="display: none"><span>{{printf "%s" .}}</span></pre>{{end}}
   181  `
   182  
   183  // codeLine represents a line of code extracted from a source file.
   184  type codeLine struct {
   185  	L  string // The line of code.
   186  	N  int    // The line number from the source file.
   187  	HL bool   // Whether the line should be highlighted.
   188  }
   189  
   190  // codeLines takes a source file and returns the lines that
   191  // span the byte range specified by start and end.
   192  // It discards lines that end in "OMIT".
   193  func codeLines(src []byte, start, end int) (lines []codeLine) {
   194  	startLine := 1
   195  	for i, b := range src {
   196  		if i == start {
   197  			break
   198  		}
   199  		if b == '\n' {
   200  			startLine++
   201  		}
   202  	}
   203  	s := bufio.NewScanner(bytes.NewReader(src[start:end]))
   204  	for n := startLine; s.Scan(); n++ {
   205  		l := s.Text()
   206  		if strings.HasSuffix(l, "OMIT") {
   207  			continue
   208  		}
   209  		lines = append(lines, codeLine{L: l, N: n})
   210  	}
   211  	// Trim leading and trailing blank lines.
   212  	for len(lines) > 0 && len(lines[0].L) == 0 {
   213  		lines = lines[1:]
   214  	}
   215  	for len(lines) > 0 && len(lines[len(lines)-1].L) == 0 {
   216  		lines = lines[:len(lines)-1]
   217  	}
   218  	return
   219  }
   220  
   221  func parseArgs(name string, line int, args []string) (res []interface{}, err error) {
   222  	res = make([]interface{}, len(args))
   223  	for i, v := range args {
   224  		if len(v) == 0 {
   225  			return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v)
   226  		}
   227  		switch v[0] {
   228  		case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
   229  			n, err := strconv.Atoi(v)
   230  			if err != nil {
   231  				return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v)
   232  			}
   233  			res[i] = n
   234  		case '/':
   235  			if len(v) < 2 || v[len(v)-1] != '/' {
   236  				return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v)
   237  			}
   238  			res[i] = v
   239  		case '$':
   240  			res[i] = "$"
   241  		case '_':
   242  			if len(v) == 1 {
   243  				// Do nothing; "_" indicates an intentionally empty parameter.
   244  				break
   245  			}
   246  			fallthrough
   247  		default:
   248  			return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v)
   249  		}
   250  	}
   251  	return
   252  }
   253  
   254  // parseArg returns the integer or string value of the argument and tells which it is.
   255  func parseArg(arg interface{}, max int) (ival int, sval string, isInt bool, err error) {
   256  	switch n := arg.(type) {
   257  	case int:
   258  		if n <= 0 || n > max {
   259  			return 0, "", false, fmt.Errorf("%d is out of range", n)
   260  		}
   261  		return n, "", true, nil
   262  	case string:
   263  		return 0, n, false, nil
   264  	}
   265  	return 0, "", false, fmt.Errorf("unrecognized argument %v type %T", arg, arg)
   266  }
   267  
   268  // match identifies the input line that matches the pattern in a code invocation.
   269  // If start>0, match lines starting there rather than at the beginning.
   270  // The return value is 1-indexed.
   271  func match(file string, start int, lines []string, pattern string) (int, error) {
   272  	// $ matches the end of the file.
   273  	if pattern == "$" {
   274  		if len(lines) == 0 {
   275  			return 0, fmt.Errorf("%q: empty file", file)
   276  		}
   277  		return len(lines), nil
   278  	}
   279  	// /regexp/ matches the line that matches the regexp.
   280  	if len(pattern) > 2 && pattern[0] == '/' && pattern[len(pattern)-1] == '/' {
   281  		re, err := regexp.Compile(pattern[1 : len(pattern)-1])
   282  		if err != nil {
   283  			return 0, err
   284  		}
   285  		for i := start; i < len(lines); i++ {
   286  			if re.MatchString(lines[i]) {
   287  				return i + 1, nil
   288  			}
   289  		}
   290  		return 0, fmt.Errorf("%s: no match for %#q", file, pattern)
   291  	}
   292  	return 0, fmt.Errorf("unrecognized pattern: %q", pattern)
   293  }