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