LEFT | RIGHT |
(no file at all) | |
| 1 // Copyright 2011 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 // This file implements FormatSelections and FormatText. |
| 6 // FormatText is used to HTML-format Go and non-Go source |
| 7 // text with line numbers and highlighted sections. It is |
| 8 // built on top of FormatSelections, a generic formatter |
| 9 // for "selected" text. |
| 10 |
| 11 package main |
| 12 |
| 13 import ( |
| 14 "bytes" |
| 15 "fmt" |
| 16 "go/scanner" |
| 17 "go/token" |
| 18 "io" |
| 19 "regexp" |
| 20 "strconv" |
| 21 "template" |
| 22 ) |
| 23 |
| 24 |
| 25 // ---------------------------------------------------------------------------- |
| 26 // Implementation of FormatSelections |
| 27 |
| 28 // A Selection is a function returning offset pairs []int{a, b} |
| 29 // describing consecutive non-overlapping text segments [a, b). |
| 30 // If there are no more segments, a Selection must return nil. |
| 31 // |
| 32 // TODO It's more efficient to return a pair (a, b int) instead |
| 33 // of creating lots of slices. Need to determine how to |
| 34 // indicate the end of a Selection. |
| 35 // |
| 36 type Selection func() []int |
| 37 |
| 38 |
| 39 // A LinkWriter writes some start or end "tag" to w for the text offset offs. |
| 40 // It is called by FormatSelections at the start or end of each link segment. |
| 41 // |
| 42 type LinkWriter func(w io.Writer, offs int, start bool) |
| 43 |
| 44 |
| 45 // A SegmentWriter formats a text according to selections and writes it to w. |
| 46 // The selections parameter is a bit set indicating which selections provided |
| 47 // to FormatSelections overlap with the text segment: If the n'th bit is set |
| 48 // in selections, the n'th selection provided to FormatSelections is overlapping |
| 49 // with the text. |
| 50 // |
| 51 type SegmentWriter func(w io.Writer, text []byte, selections int) |
| 52 |
| 53 |
| 54 // FormatSelections takes a text and writes it to w using link and segment |
| 55 // writers lw and sw as follows: lw is invoked for consecutive segment starts |
| 56 // and ends as specified through the links selection, and sw is invoked for |
| 57 // consecutive segments of text overlapped by the same selections as specified |
| 58 // by selections. The link writer lw may be nil, in which case the links |
| 59 // Selection is ignored. |
| 60 // |
| 61 func FormatSelections(w io.Writer, text []byte, lw LinkWriter, links Selection,
sw SegmentWriter, selections ...Selection) { |
| 62 if lw != nil { |
| 63 selections = append(selections, links) |
| 64 } |
| 65 // compute the sequence of consecutive segment changes |
| 66 changes := newMerger(selections) |
| 67 // The i'th bit in bitset indicates that the text |
| 68 // at the current offset is covered by selections[i]. |
| 69 bitset := 0 |
| 70 lastOffs := 0 |
| 71 for { |
| 72 // get the next segment change |
| 73 index, offs, start := changes.next() |
| 74 if index < 0 || offs > len(text) { |
| 75 // no more segment changes or the next change |
| 76 // is past the end of the text - we're done |
| 77 break |
| 78 } |
| 79 // determine the kind of segment change |
| 80 if index == len(selections)-1 { |
| 81 // we have a link segment change: |
| 82 // format the previous selection segment, write the |
| 83 // link tag and start a new selection segment |
| 84 sw(w, text[lastOffs:offs], bitset) |
| 85 lastOffs = offs |
| 86 lw(w, offs, start) |
| 87 } else { |
| 88 // we have a selection change: |
| 89 // format the previous selection segment, determine |
| 90 // the new selection bitset and start a new segment· |
| 91 sw(w, text[lastOffs:offs], bitset) |
| 92 lastOffs = offs |
| 93 mask := 1 << uint(index) |
| 94 if start { |
| 95 bitset |= mask |
| 96 } else { |
| 97 bitset &^= mask |
| 98 } |
| 99 } |
| 100 } |
| 101 sw(w, text[lastOffs:], bitset) |
| 102 } |
| 103 |
| 104 |
| 105 // A merger merges a slice of Selections and produces a sequence of |
| 106 // consecutive segment change events through repeated next() calls. |
| 107 // |
| 108 type merger struct { |
| 109 selections []Selection |
| 110 segments [][]int // segments[i] is the next segment of selections[i] |
| 111 } |
| 112 |
| 113 |
| 114 const infinity int = 2e9 |
| 115 |
| 116 func newMerger(selections []Selection) *merger { |
| 117 segments := make([][]int, len(selections)) |
| 118 for i, sel := range selections { |
| 119 segments[i] = []int{infinity, infinity} |
| 120 if sel != nil { |
| 121 if seg := sel(); seg != nil { |
| 122 segments[i] = seg |
| 123 } |
| 124 } |
| 125 } |
| 126 return &merger{selections, segments} |
| 127 } |
| 128 |
| 129 |
| 130 // next returns the next segment change: index specifies the Selection |
| 131 // to which the segment belongs, offs is the segment start or end offset |
| 132 // as determined by the start value. If there are no more segment changes, |
| 133 // next returns an index value < 0. |
| 134 // |
| 135 func (m *merger) next() (index, offs int, start bool) { |
| 136 // find the next smallest offset where a segment starts or ends |
| 137 offs = infinity |
| 138 index = -1 |
| 139 for i, seg := range m.segments { |
| 140 switch { |
| 141 case seg[0] < offs: |
| 142 offs = seg[0] |
| 143 index = i |
| 144 start = true |
| 145 case seg[1] < offs: |
| 146 offs = seg[1] |
| 147 index = i |
| 148 start = false |
| 149 } |
| 150 } |
| 151 if index < 0 { |
| 152 // no offset found => all selections merged |
| 153 return |
| 154 } |
| 155 // offset found - it's either the start or end offset but |
| 156 // either way it is ok to consume the start offset: set it |
| 157 // to infinity so it won't be considered in the following |
| 158 // next call |
| 159 m.segments[index][0] = infinity |
| 160 if start { |
| 161 return |
| 162 } |
| 163 // end offset found - consume it |
| 164 m.segments[index][1] = infinity |
| 165 // advance to the next segment for that selection |
| 166 seg := m.selections[index]() |
| 167 if seg == nil { |
| 168 return |
| 169 } |
| 170 m.segments[index] = seg |
| 171 return |
| 172 } |
| 173 |
| 174 |
| 175 // ---------------------------------------------------------------------------- |
| 176 // Implementation of FormatText |
| 177 |
| 178 // lineSelection returns the line segments for text as a Selection. |
| 179 func lineSelection(text []byte) Selection { |
| 180 i, j := 0, 0 |
| 181 return func() (seg []int) { |
| 182 // find next newline, if any |
| 183 for j < len(text) { |
| 184 j++ |
| 185 if text[j-1] == '\n' { |
| 186 break |
| 187 } |
| 188 } |
| 189 if i < j { |
| 190 // text[i:j] constitutes a line |
| 191 seg = []int{i, j} |
| 192 i = j |
| 193 } |
| 194 return |
| 195 } |
| 196 } |
| 197 |
| 198 |
| 199 // commentSelection returns the sequence of consecutive comments |
| 200 // in the Go src text as a Selection. |
| 201 // |
| 202 func commentSelection(src []byte) Selection { |
| 203 var s scanner.Scanner |
| 204 file := s.Init(token.NewFileSet(), "", src, nil, scanner.ScanComments+sc
anner.InsertSemis) |
| 205 return func() (seg []int) { |
| 206 for { |
| 207 pos, tok, lit := s.Scan() |
| 208 if tok == token.EOF { |
| 209 break |
| 210 } |
| 211 offs := file.Offset(pos) |
| 212 if tok == token.COMMENT { |
| 213 seg = []int{offs, offs + len(lit)} |
| 214 break |
| 215 } |
| 216 } |
| 217 return |
| 218 } |
| 219 } |
| 220 |
| 221 |
| 222 // makeSelection is a helper function to make a Selection from a slice of pairs. |
| 223 func makeSelection(matches [][]int) Selection { |
| 224 return func() (seg []int) { |
| 225 if len(matches) > 0 { |
| 226 seg = matches[0] |
| 227 matches = matches[1:] |
| 228 } |
| 229 return |
| 230 } |
| 231 } |
| 232 |
| 233 |
| 234 // regexpSelection computes the Selection for the regular expression expr in tex
t. |
| 235 func regexpSelection(text []byte, expr string) Selection { |
| 236 var matches [][]int |
| 237 if rx, err := regexp.Compile(expr); err == nil { |
| 238 matches = rx.FindAllIndex(text, -1) |
| 239 } |
| 240 return makeSelection(matches) |
| 241 } |
| 242 |
| 243 |
| 244 var selRx = regexp.MustCompile(`^([0-9]+):([0-9]+)`) |
| 245 |
| 246 // rangeSelection computes the Selection for a text range described |
| 247 // by the argument str; the range description must match the selRx |
| 248 // regular expression. |
| 249 // |
| 250 func rangeSelection(str string) Selection { |
| 251 m := selRx.FindStringSubmatch(str) |
| 252 if len(m) >= 2 { |
| 253 from, _ := strconv.Atoi(m[1]) |
| 254 to, _ := strconv.Atoi(m[2]) |
| 255 if from < to { |
| 256 return makeSelection([][]int{[]int{from, to}}) |
| 257 } |
| 258 } |
| 259 return nil |
| 260 } |
| 261 |
| 262 |
| 263 // Span tags for all the possible selection combinations that may |
| 264 // be generated by FormatText. Selections are indicated by a bitset, |
| 265 // and the value of the bitset specifies the tag to be used. |
| 266 // |
| 267 // bit 0: comments |
| 268 // bit 1: highlights |
| 269 // bit 2: selections |
| 270 // |
| 271 var startTags = [][]byte{ |
| 272 /* 000 */ []byte(``), |
| 273 /* 001 */ []byte(`<span class ="comment">`), |
| 274 /* 010 */ []byte(`<span class="highlight">`), |
| 275 /* 011 */ []byte(`<span class="highlight-comment">`), |
| 276 /* 100 */ []byte(`<span class="selection">`), |
| 277 /* 101 */ []byte(`<span class="selection-comment">`), |
| 278 /* 110 */ []byte(`<span class="selection-highlight">`), |
| 279 /* 111 */ []byte(`<span class="selection-highlight-comment">`), |
| 280 } |
| 281 |
| 282 var endTag = []byte(`</span>`) |
| 283 |
| 284 |
| 285 func selectionTag(w io.Writer, text []byte, selections int) { |
| 286 if len(text) > 0 { |
| 287 if selections < len(startTags) { |
| 288 if tag := startTags[selections]; len(tag) > 0 { |
| 289 w.Write(tag) |
| 290 template.HTMLEscape(w, text) |
| 291 w.Write(endTag) |
| 292 return |
| 293 } |
| 294 } |
| 295 template.HTMLEscape(w, text) |
| 296 } |
| 297 } |
| 298 |
| 299 |
| 300 // FormatText HTML-escapes text and returns it wrapped in <pre> tags. |
| 301 // Conscutive text segments are wrapped in HTML spans (with tags as |
| 302 // defined by startTags and endTag) as follows: |
| 303 // |
| 304 // - if line >= 0, line numbers are printed before each line, starting |
| 305 // with the value of line |
| 306 // - if the text is Go source, comments get the "comment" span class |
| 307 // - each occurrence of the regular expression pattern gets the "highlight" |
| 308 // span class |
| 309 // - text segments covered by selection get the "selection" span class |
| 310 // |
| 311 // Comments, highlights, and selections may overlap arbitrarily; the respective |
| 312 // HTML span classes are specified in the startTags variable. |
| 313 // |
| 314 func FormatText(text []byte, line int, goSource bool, pattern string, selection
Selection) []byte { |
| 315 var buf bytes.Buffer |
| 316 buf.WriteString("<pre>\n") |
| 317 |
| 318 var comments, highlights Selection |
| 319 if goSource { |
| 320 comments = commentSelection(text) |
| 321 } |
| 322 if pattern != "" { |
| 323 highlights = regexpSelection(text, pattern) |
| 324 } |
| 325 if comments != nil || highlights != nil || selection != nil { |
| 326 var lineTag LinkWriter |
| 327 if line >= 0 { |
| 328 lineTag = func(w io.Writer, _ int, start bool) { |
| 329 if start { |
| 330 fmt.Fprintf(w, "<a id=\"L%d\"></a>%5d\t"
, line, line) |
| 331 line++ |
| 332 } |
| 333 } |
| 334 } |
| 335 FormatSelections(&buf, text, lineTag, lineSelection(text), selec
tionTag, comments, highlights, selection) |
| 336 } else { |
| 337 template.HTMLEscape(&buf, text) |
| 338 } |
| 339 |
| 340 buf.WriteString("</pre>\n") |
| 341 return buf.Bytes() |
| 342 } |
LEFT | RIGHT |