...

Source file src/golang.org/x/tools/present/code.go

Documentation: golang.org/x/tools/present

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

View as plain text