372 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			372 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
// Copyright 2011 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.
 | 
						|
 | 
						|
// This file implements FormatSelections and FormatText.
 | 
						|
// FormatText is used to HTML-format Go and non-Go source
 | 
						|
// text with line numbers and highlighted sections. It is
 | 
						|
// built on top of FormatSelections, a generic formatter
 | 
						|
// for "selected" text.
 | 
						|
 | 
						|
package godoc
 | 
						|
 | 
						|
import (
 | 
						|
	"fmt"
 | 
						|
	"go/scanner"
 | 
						|
	"go/token"
 | 
						|
	"io"
 | 
						|
	"regexp"
 | 
						|
	"strconv"
 | 
						|
	"text/template"
 | 
						|
)
 | 
						|
 | 
						|
// ----------------------------------------------------------------------------
 | 
						|
// Implementation of FormatSelections
 | 
						|
 | 
						|
// A Segment describes a text segment [start, end).
 | 
						|
// The zero value of a Segment is a ready-to-use empty segment.
 | 
						|
//
 | 
						|
type Segment struct {
 | 
						|
	start, end int
 | 
						|
}
 | 
						|
 | 
						|
func (seg *Segment) isEmpty() bool { return seg.start >= seg.end }
 | 
						|
 | 
						|
// A Selection is an "iterator" function returning a text segment.
 | 
						|
// Repeated calls to a selection return consecutive, non-overlapping,
 | 
						|
// non-empty segments, followed by an infinite sequence of empty
 | 
						|
// segments. The first empty segment marks the end of the selection.
 | 
						|
//
 | 
						|
type Selection func() Segment
 | 
						|
 | 
						|
// A LinkWriter writes some start or end "tag" to w for the text offset offs.
 | 
						|
// It is called by FormatSelections at the start or end of each link segment.
 | 
						|
//
 | 
						|
type LinkWriter func(w io.Writer, offs int, start bool)
 | 
						|
 | 
						|
// A SegmentWriter formats a text according to selections and writes it to w.
 | 
						|
// The selections parameter is a bit set indicating which selections provided
 | 
						|
// to FormatSelections overlap with the text segment: If the n'th bit is set
 | 
						|
// in selections, the n'th selection provided to FormatSelections is overlapping
 | 
						|
// with the text.
 | 
						|
//
 | 
						|
type SegmentWriter func(w io.Writer, text []byte, selections int)
 | 
						|
 | 
						|
// FormatSelections takes a text and writes it to w using link and segment
 | 
						|
// writers lw and sw as follows: lw is invoked for consecutive segment starts
 | 
						|
// and ends as specified through the links selection, and sw is invoked for
 | 
						|
// consecutive segments of text overlapped by the same selections as specified
 | 
						|
// by selections. The link writer lw may be nil, in which case the links
 | 
						|
// Selection is ignored.
 | 
						|
//
 | 
						|
func FormatSelections(w io.Writer, text []byte, lw LinkWriter, links Selection, sw SegmentWriter, selections ...Selection) {
 | 
						|
	// If we have a link writer, make the links
 | 
						|
	// selection the last entry in selections
 | 
						|
	if lw != nil {
 | 
						|
		selections = append(selections, links)
 | 
						|
	}
 | 
						|
 | 
						|
	// compute the sequence of consecutive segment changes
 | 
						|
	changes := newMerger(selections)
 | 
						|
 | 
						|
	// The i'th bit in bitset indicates that the text
 | 
						|
	// at the current offset is covered by selections[i].
 | 
						|
	bitset := 0
 | 
						|
	lastOffs := 0
 | 
						|
 | 
						|
	// Text segments are written in a delayed fashion
 | 
						|
	// such that consecutive segments belonging to the
 | 
						|
	// same selection can be combined (peephole optimization).
 | 
						|
	// last describes the last segment which has not yet been written.
 | 
						|
	var last struct {
 | 
						|
		begin, end int // valid if begin < end
 | 
						|
		bitset     int
 | 
						|
	}
 | 
						|
 | 
						|
	// flush writes the last delayed text segment
 | 
						|
	flush := func() {
 | 
						|
		if last.begin < last.end {
 | 
						|
			sw(w, text[last.begin:last.end], last.bitset)
 | 
						|
		}
 | 
						|
		last.begin = last.end // invalidate last
 | 
						|
	}
 | 
						|
 | 
						|
	// segment runs the segment [lastOffs, end) with the selection
 | 
						|
	// indicated by bitset through the segment peephole optimizer.
 | 
						|
	segment := func(end int) {
 | 
						|
		if lastOffs < end { // ignore empty segments
 | 
						|
			if last.end != lastOffs || last.bitset != bitset {
 | 
						|
				// the last segment is not adjacent to or
 | 
						|
				// differs from the new one
 | 
						|
				flush()
 | 
						|
				// start a new segment
 | 
						|
				last.begin = lastOffs
 | 
						|
			}
 | 
						|
			last.end = end
 | 
						|
			last.bitset = bitset
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	for {
 | 
						|
		// get the next segment change
 | 
						|
		index, offs, start := changes.next()
 | 
						|
		if index < 0 || offs > len(text) {
 | 
						|
			// no more segment changes or the next change
 | 
						|
			// is past the end of the text - we're done
 | 
						|
			break
 | 
						|
		}
 | 
						|
		// determine the kind of segment change
 | 
						|
		if lw != nil && index == len(selections)-1 {
 | 
						|
			// we have a link segment change (see start of this function):
 | 
						|
			// format the previous selection segment, write the
 | 
						|
			// link tag and start a new selection segment
 | 
						|
			segment(offs)
 | 
						|
			flush()
 | 
						|
			lastOffs = offs
 | 
						|
			lw(w, offs, start)
 | 
						|
		} else {
 | 
						|
			// we have a selection change:
 | 
						|
			// format the previous selection segment, determine
 | 
						|
			// the new selection bitset and start a new segment
 | 
						|
			segment(offs)
 | 
						|
			lastOffs = offs
 | 
						|
			mask := 1 << uint(index)
 | 
						|
			if start {
 | 
						|
				bitset |= mask
 | 
						|
			} else {
 | 
						|
				bitset &^= mask
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	segment(len(text))
 | 
						|
	flush()
 | 
						|
}
 | 
						|
 | 
						|
// A merger merges a slice of Selections and produces a sequence of
 | 
						|
// consecutive segment change events through repeated next() calls.
 | 
						|
//
 | 
						|
type merger struct {
 | 
						|
	selections []Selection
 | 
						|
	segments   []Segment // segments[i] is the next segment of selections[i]
 | 
						|
}
 | 
						|
 | 
						|
const infinity int = 2e9
 | 
						|
 | 
						|
func newMerger(selections []Selection) *merger {
 | 
						|
	segments := make([]Segment, len(selections))
 | 
						|
	for i, sel := range selections {
 | 
						|
		segments[i] = Segment{infinity, infinity}
 | 
						|
		if sel != nil {
 | 
						|
			if seg := sel(); !seg.isEmpty() {
 | 
						|
				segments[i] = seg
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return &merger{selections, segments}
 | 
						|
}
 | 
						|
 | 
						|
// next returns the next segment change: index specifies the Selection
 | 
						|
// to which the segment belongs, offs is the segment start or end offset
 | 
						|
// as determined by the start value. If there are no more segment changes,
 | 
						|
// next returns an index value < 0.
 | 
						|
//
 | 
						|
func (m *merger) next() (index, offs int, start bool) {
 | 
						|
	// find the next smallest offset where a segment starts or ends
 | 
						|
	offs = infinity
 | 
						|
	index = -1
 | 
						|
	for i, seg := range m.segments {
 | 
						|
		switch {
 | 
						|
		case seg.start < offs:
 | 
						|
			offs = seg.start
 | 
						|
			index = i
 | 
						|
			start = true
 | 
						|
		case seg.end < offs:
 | 
						|
			offs = seg.end
 | 
						|
			index = i
 | 
						|
			start = false
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if index < 0 {
 | 
						|
		// no offset found => all selections merged
 | 
						|
		return
 | 
						|
	}
 | 
						|
	// offset found - it's either the start or end offset but
 | 
						|
	// either way it is ok to consume the start offset: set it
 | 
						|
	// to infinity so it won't be considered in the following
 | 
						|
	// next call
 | 
						|
	m.segments[index].start = infinity
 | 
						|
	if start {
 | 
						|
		return
 | 
						|
	}
 | 
						|
	// end offset found - consume it
 | 
						|
	m.segments[index].end = infinity
 | 
						|
	// advance to the next segment for that selection
 | 
						|
	seg := m.selections[index]()
 | 
						|
	if !seg.isEmpty() {
 | 
						|
		m.segments[index] = seg
 | 
						|
	}
 | 
						|
	return
 | 
						|
}
 | 
						|
 | 
						|
// ----------------------------------------------------------------------------
 | 
						|
// Implementation of FormatText
 | 
						|
 | 
						|
// lineSelection returns the line segments for text as a Selection.
 | 
						|
func lineSelection(text []byte) Selection {
 | 
						|
	i, j := 0, 0
 | 
						|
	return func() (seg Segment) {
 | 
						|
		// find next newline, if any
 | 
						|
		for j < len(text) {
 | 
						|
			j++
 | 
						|
			if text[j-1] == '\n' {
 | 
						|
				break
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if i < j {
 | 
						|
			// text[i:j] constitutes a line
 | 
						|
			seg = Segment{i, j}
 | 
						|
			i = j
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// tokenSelection returns, as a selection, the sequence of
 | 
						|
// consecutive occurrences of token sel in the Go src text.
 | 
						|
//
 | 
						|
func tokenSelection(src []byte, sel token.Token) Selection {
 | 
						|
	var s scanner.Scanner
 | 
						|
	fset := token.NewFileSet()
 | 
						|
	file := fset.AddFile("", fset.Base(), len(src))
 | 
						|
	s.Init(file, src, nil, scanner.ScanComments)
 | 
						|
	return func() (seg Segment) {
 | 
						|
		for {
 | 
						|
			pos, tok, lit := s.Scan()
 | 
						|
			if tok == token.EOF {
 | 
						|
				break
 | 
						|
			}
 | 
						|
			offs := file.Offset(pos)
 | 
						|
			if tok == sel {
 | 
						|
				seg = Segment{offs, offs + len(lit)}
 | 
						|
				break
 | 
						|
			}
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// makeSelection is a helper function to make a Selection from a slice of pairs.
 | 
						|
// Pairs describing empty segments are ignored.
 | 
						|
//
 | 
						|
func makeSelection(matches [][]int) Selection {
 | 
						|
	i := 0
 | 
						|
	return func() Segment {
 | 
						|
		for i < len(matches) {
 | 
						|
			m := matches[i]
 | 
						|
			i++
 | 
						|
			if m[0] < m[1] {
 | 
						|
				// non-empty segment
 | 
						|
				return Segment{m[0], m[1]}
 | 
						|
			}
 | 
						|
		}
 | 
						|
		return Segment{}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// regexpSelection computes the Selection for the regular expression expr in text.
 | 
						|
func regexpSelection(text []byte, expr string) Selection {
 | 
						|
	var matches [][]int
 | 
						|
	if rx, err := regexp.Compile(expr); err == nil {
 | 
						|
		matches = rx.FindAllIndex(text, -1)
 | 
						|
	}
 | 
						|
	return makeSelection(matches)
 | 
						|
}
 | 
						|
 | 
						|
var selRx = regexp.MustCompile(`^([0-9]+):([0-9]+)`)
 | 
						|
 | 
						|
// RangeSelection computes the Selection for a text range described
 | 
						|
// by the argument str; the range description must match the selRx
 | 
						|
// regular expression.
 | 
						|
func RangeSelection(str string) Selection {
 | 
						|
	m := selRx.FindStringSubmatch(str)
 | 
						|
	if len(m) >= 2 {
 | 
						|
		from, _ := strconv.Atoi(m[1])
 | 
						|
		to, _ := strconv.Atoi(m[2])
 | 
						|
		if from < to {
 | 
						|
			return makeSelection([][]int{{from, to}})
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// Span tags for all the possible selection combinations that may
 | 
						|
// be generated by FormatText. Selections are indicated by a bitset,
 | 
						|
// and the value of the bitset specifies the tag to be used.
 | 
						|
//
 | 
						|
// bit 0: comments
 | 
						|
// bit 1: highlights
 | 
						|
// bit 2: selections
 | 
						|
//
 | 
						|
var startTags = [][]byte{
 | 
						|
	/* 000 */ []byte(``),
 | 
						|
	/* 001 */ []byte(`<span class="comment">`),
 | 
						|
	/* 010 */ []byte(`<span class="highlight">`),
 | 
						|
	/* 011 */ []byte(`<span class="highlight-comment">`),
 | 
						|
	/* 100 */ []byte(`<span class="selection">`),
 | 
						|
	/* 101 */ []byte(`<span class="selection-comment">`),
 | 
						|
	/* 110 */ []byte(`<span class="selection-highlight">`),
 | 
						|
	/* 111 */ []byte(`<span class="selection-highlight-comment">`),
 | 
						|
}
 | 
						|
 | 
						|
var endTag = []byte(`</span>`)
 | 
						|
 | 
						|
func selectionTag(w io.Writer, text []byte, selections int) {
 | 
						|
	if selections < len(startTags) {
 | 
						|
		if tag := startTags[selections]; len(tag) > 0 {
 | 
						|
			w.Write(tag)
 | 
						|
			template.HTMLEscape(w, text)
 | 
						|
			w.Write(endTag)
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
	template.HTMLEscape(w, text)
 | 
						|
}
 | 
						|
 | 
						|
// FormatText HTML-escapes text and writes it to w.
 | 
						|
// Consecutive text segments are wrapped in HTML spans (with tags as
 | 
						|
// defined by startTags and endTag) as follows:
 | 
						|
//
 | 
						|
//	- if line >= 0, line number (ln) spans are inserted before each line,
 | 
						|
//	  starting with the value of line
 | 
						|
//	- if the text is Go source, comments get the "comment" span class
 | 
						|
//	- each occurrence of the regular expression pattern gets the "highlight"
 | 
						|
//	  span class
 | 
						|
//	- text segments covered by selection get the "selection" span class
 | 
						|
//
 | 
						|
// Comments, highlights, and selections may overlap arbitrarily; the respective
 | 
						|
// HTML span classes are specified in the startTags variable.
 | 
						|
//
 | 
						|
func FormatText(w io.Writer, text []byte, line int, goSource bool, pattern string, selection Selection) {
 | 
						|
	var comments, highlights Selection
 | 
						|
	if goSource {
 | 
						|
		comments = tokenSelection(text, token.COMMENT)
 | 
						|
	}
 | 
						|
	if pattern != "" {
 | 
						|
		highlights = regexpSelection(text, pattern)
 | 
						|
	}
 | 
						|
	if line >= 0 || comments != nil || highlights != nil || selection != nil {
 | 
						|
		var lineTag LinkWriter
 | 
						|
		if line >= 0 {
 | 
						|
			lineTag = func(w io.Writer, _ int, start bool) {
 | 
						|
				if start {
 | 
						|
					fmt.Fprintf(w, "<span id=\"L%d\" class=\"ln\">%6d</span>\t", line, line)
 | 
						|
					line++
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
		FormatSelections(w, text, lineTag, lineSelection(text), selectionTag, comments, highlights, selection)
 | 
						|
	} else {
 | 
						|
		template.HTMLEscape(w, text)
 | 
						|
	}
 | 
						|
}
 |