main ab6686c

parser.go RawHistoryFolder

1package kittygraphics
2
3import "bytes"
4
5var apcPrefix = [...]byte{0x1b, '_', 'G'}
6
7// Feed consumes data and returns text and complete graphics events. The
8// parser retains only an incomplete APC or a possible split prefix between
9// calls. A malformed APC is reported as an EventError and discarded through
10// its string terminator, allowing following text and APCs to be processed.
11func (p *Parser) Feed(data []byte) []Event {
12	if p == nil {
13		if len(data) == 0 {
14			return nil
15		}
16		return []Event{{Kind: EventText, Text: append([]byte(nil), data...)}}
17	}
18	var events []Event
19	text := make([]byte, 0, len(data))
20	flushText := func() {
21		if len(text) == 0 {
22			return
23		}
24		events = append(events, Event{Kind: EventText, Text: append([]byte(nil), text...)})
25		text = text[:0]
26	}
27
28	for _, b := range data {
29		if !p.inAPC {
30			p.candidate = append(p.candidate, b)
31			for len(p.candidate) != 0 {
32				if len(p.candidate) <= len(apcPrefix) && bytes.Equal(p.candidate, apcPrefix[:len(p.candidate)]) {
33					break
34				}
35				text = append(text, p.candidate[0])
36				p.candidate = p.candidate[1:]
37			}
38			if len(p.candidate) == len(apcPrefix) {
39				flushText()
40				p.candidate = p.candidate[:0]
41				p.inAPC = true
42				p.escaped = false
43				p.oversize = false
44				p.apc = p.apc[:0]
45			}
46			continue
47		}
48
49		if p.escaped {
50			if b == '\\' {
51				if p.oversize {
52					events = append(events, p.errorEvent())
53				} else {
54					events = append(events, p.commandEvent())
55				}
56				p.resetAPC()
57				continue
58			}
59			// An ESC that is not followed by backslash is payload. Keep the
60			// byte and continue looking for a terminator. This is necessary
61			// for raw payloads and is harmless for base64 payloads.
62			p.appendAPC(0x1b)
63			p.escaped = false
64		}
65		if b == 0x9c { // C1 ST is also a valid APC terminator.
66			if p.oversize {
67				events = append(events, p.errorEvent())
68			} else {
69				events = append(events, p.commandEvent())
70			}
71			p.resetAPC()
72			continue
73		}
74		if b == 0x1b {
75			p.escaped = true
76			continue
77		}
78		p.appendAPC(b)
79	}
80	flushText()
81	return events
82}
83
84// Parse is an alias for Feed.
85func (p *Parser) Parse(data []byte) []Event { return p.Feed(data) }
86
87// FeedWithErrors is a conventional error-returning facade over Feed. Parser
88// errors are also retained in the returned Event values.
89func (p *Parser) FeedWithErrors(data []byte) ([]Event, error) {
90	events := p.Feed(data)
91	for _, event := range events {
92		if event.Kind == EventError && event.Err != nil {
93			return events, event.Err
94		}
95	}
96	return events, nil
97}
98
99// Finish reports an unterminated APC and resets the parser. Text which is a
100// partial ordinary escape sequence is returned as text. Finish is useful at a
101// stream boundary; a caller that will feed more data must not call it.
102func (p *Parser) Finish() []Event {
103	if p == nil {
104		return nil
105	}
106	var events []Event
107	if p.inAPC {
108		err := ErrAPCTruncated
109		if p.oversize {
110			err = ErrAPCTooLarge
111		}
112		events = append(events, Event{Kind: EventError, Err: err})
113		p.resetAPC()
114	}
115	if len(p.candidate) != 0 {
116		events = append(events, Event{Kind: EventText, Text: append([]byte(nil), p.candidate...)})
117		p.candidate = p.candidate[:0]
118	}
119	return events
120}
121
122// Flush is an alias for Finish.
123func (p *Parser) Flush() []Event { return p.Finish() }
124
125func (p *Parser) appendAPC(b byte) {
126	if p.oversize {
127		return
128	}
129	if uint64(len(p.apc))+1 > p.limits.MaxAPCBytes {
130		p.oversize = true
131		return
132	}
133	p.apc = append(p.apc, b)
134}
135
136func (p *Parser) resetAPC() {
137	p.inAPC = false
138	p.escaped = false
139	p.oversize = false
140	p.apc = p.apc[:0]
141}
142
143func (p *Parser) errorEvent() Event {
144	if p.oversize {
145		return Event{Kind: EventError, Err: ErrAPCTooLarge}
146	}
147	return Event{Kind: EventError, Err: ErrInvalidAPC}
148}
149
150func (p *Parser) commandEvent() Event {
151	command, err := ParseCommand(p.apc, p.limits)
152	if err != nil {
153		return Event{Kind: EventError, Err: err}
154	}
155	return Event{Kind: EventCommand, Command: command}
156}
157
158// ParseCommand parses the bytes between APC's G and its ST terminator. It is
159// exported separately so adapters can validate commands without maintaining a
160// stream parser.
161func ParseCommand(body []byte, config ...Limits) (Command, error) {
162	return parseCommand(body, true, config)
163}
164
165func parseCommand(body []byte, copyPayload bool, config []Limits) (Command, error) {
166	var l Limits
167	if len(config) != 0 {
168		l = config[0]
169	}
170	l = normalizeLimits(l)
171	if uint64(len(body)) > l.MaxAPCBytes {
172		return Command{}, ErrAPCTooLarge
173	}
174	controlEnd, payloadStart := len(body), len(body)
175	if sep := bytes.IndexByte(body, ';'); sep >= 0 {
176		controlEnd, payloadStart = sep, sep+1
177	}
178	controls, err := parseControls(body[:controlEnd])
179	if err != nil {
180		return Command{}, err
181	}
182	payload := body[payloadStart:]
183	if uint64(len(payload)) > l.MaxPayloadBytes {
184		return Command{}, ErrPayloadTooLarge
185	}
186	if copyPayload {
187		payload = append([]byte(nil), payload...)
188	}
189	return Command{Controls: controls, Payload: payload}, nil
190}
191
192// ParseAPC parses a complete ESC _ G ... ST sequence.
193func ParseAPC(apc []byte, config ...Limits) (Command, error) {
194	return parseAPC(apc, true, config)
195}
196
197// ParseAPCBorrowed is ParseAPC without copying the payload: the returned
198// Command.Payload aliases apc. The caller must not modify apc while the
199// command is in use and must not retain the command beyond the lifetime of
200// apc. Session.Process copies whatever it keeps from a command's payload, so
201// a borrowed command may be passed to it and apc reused afterwards.
202func ParseAPCBorrowed(apc []byte, config ...Limits) (Command, error) {
203	return parseAPC(apc, false, config)
204}
205
206func parseAPC(apc []byte, copyPayload bool, config []Limits) (Command, error) {
207	if len(apc) < 4 || apc[0] != 0x1b || apc[1] != '_' || apc[2] != 'G' {
208		return Command{}, ErrInvalidAPC
209	}
210	body := apc[3:]
211	if body[len(body)-1] == 0x9c {
212		body = body[:len(body)-1]
213	} else if len(body) >= 2 && body[len(body)-2] == 0x1b && body[len(body)-1] == '\\' {
214		body = body[:len(body)-2]
215	} else {
216		return Command{}, ErrAPCTruncated
217	}
218	return parseCommand(body, copyPayload, config)
219}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close