Left: | ||
Right: |
OLD | NEW |
---|---|
(Empty) | |
1 // Copyright 2010 The Go Authors. All rights reserved. | |
r
2011/01/09 00:08:55
2011?
gri
2011/01/10 23:15:01
Done.
| |
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 // bit 0: comments | |
264 // bit 1: highlights | |
265 // bit 2: selections | |
266 // | |
267 var startTags = [][]byte{ | |
268 /* 000 */ []byte(``), | |
269 /* 001 */ []byte(`<span class ="comment">`), | |
270 /* 010 */ []byte(`<span class="highlight">`), | |
271 /* 011 */ []byte(`<span class="highlight-comment">`), | |
272 /* 100 */ []byte(`<span class="selection">`), | |
273 /* 101 */ []byte(`<span class="selection-comment">`), | |
274 /* 110 */ []byte(`<span class="selection-highlight">`), | |
275 /* 111 */ []byte(`<span class="selection-highlight-comment">`), | |
276 } | |
277 | |
278 var endTag = []byte(`</span>`) | |
279 | |
280 | |
281 func selectionTag(w io.Writer, text []byte, selections int) { | |
282 if len(text) > 0 { | |
283 if selections < len(startTags) { | |
284 if tag := startTags[selections]; len(tag) > 0 { | |
285 w.Write(tag) | |
286 template.HTMLEscape(w, text) | |
287 w.Write(endTag) | |
288 return | |
289 } | |
290 } | |
291 template.HTMLEscape(w, text) | |
292 } | |
293 } | |
294 | |
295 | |
296 // FormatText HTML-escapes text and returns it wrapped in <pre> tags. | |
297 // Conscutive text segments are wrapped in HTML spans as follows: | |
298 // | |
299 // - if line >= 0, line numbers are printed before each line, starting | |
300 // with the value of line | |
301 // - if the text is Go source, comments get the "comment" span class | |
302 // - each occurence of the regular expression pattern gets the "highlight" | |
r
2011/01/09 00:08:55
s/r/rr/
gri
2011/01/10 23:15:01
Done.
| |
303 // span class | |
304 // - text segments covered by selection get the "selection" span class | |
305 // | |
306 // Comments, highlights, and selections may overlap arbitrarily; the respective | |
307 // HTML span classes are specified in the startTags variable. | |
r
2011/01/09 00:08:55
this comment should appear or at least be tied to
gri
2011/01/10 23:15:01
Done.
| |
308 // | |
309 func FormatText(text []byte, line int, goSource bool, pattern string, selection Selection) []byte { | |
310 var buf bytes.Buffer | |
311 buf.WriteString("<pre>\n") | |
312 | |
313 var comments, highlights Selection | |
314 if goSource { | |
315 comments = commentSelection(text) | |
316 } | |
317 if pattern != "" { | |
318 highlights = regexpSelection(text, pattern) | |
319 } | |
320 if comments != nil || highlights != nil || selection != nil { | |
321 var lineTag LinkWriter | |
322 if line >= 0 { | |
323 lineTag = func(w io.Writer, _ int, start bool) { | |
324 if start { | |
325 fmt.Fprintf(w, "<a id=\"L%d\"></a>%5d\t" , line, line) | |
326 line++ | |
327 } | |
328 } | |
329 } | |
330 FormatSelections(&buf, text, lineTag, lineSelection(text), selec tionTag, comments, highlights, selection) | |
331 } else { | |
332 template.HTMLEscape(&buf, text) | |
333 } | |
334 | |
335 buf.WriteString("</pre>\n") | |
336 return buf.Bytes() | |
337 } | |
OLD | NEW |