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