1package chat
2
3import (
4 "strings"
5
6 "charm.land/glamour/v2"
7 "github.com/charmbracelet/crush/internal/ui/common"
8)
9
10// streamingMarkdown caches a "stable prefix" glamour render so each
11// streaming flush only re-renders the trailing portion of the
12// document. F8 of docs/notes/2026-05-12-chat-rendering-perf.md.
13//
14// The boundary between "stable" and "trailing" is detected by
15// [findSafeMarkdownBoundary]: a position immediately after a blank
16// line at which we can prove no markdown construct is open
17// (fenced code block, list, table, block quote, setext header).
18//
19// Two renders concatenated are NOT generally equal to a single
20// render of the whole document โ glamour's wrap state is reset
21// between calls. The boundary check is therefore deliberately
22// conservative; whenever it has the slightest doubt the call
23// falls back to a full render and the cache is left untouched.
24//
25// Invariants:
26//
27// - stablePrefix is always a literal byte prefix of the most
28// recently rendered content. If a new content does not have
29// stablePrefix as its prefix the cache is dropped.
30// - stablePrefixRender is the glamour render of stablePrefix
31// alone, with surrounding whitespace trimmed for clean
32// concatenation.
33// - width is the glamour wrap width that produced
34// stablePrefixRender. A width change drops the cache.
35type streamingMarkdown struct {
36 width int
37 stablePrefix string
38 stablePrefixRender string
39}
40
41// Reset drops every cached field. After Reset the next Render call
42// is guaranteed to be a full render.
43func (s *streamingMarkdown) Reset() {
44 s.width = 0
45 s.stablePrefix = ""
46 s.stablePrefixRender = ""
47}
48
49// Render returns the glamour render of content at the given width,
50// reusing the cached stable-prefix render when it is safe to do so.
51// On any uncertainty the call falls back to a full render via
52// renderer and leaves the cache untouched (or drops it).
53//
54// The returned string has its trailing newline trimmed to match
55// the existing renderMarkdown contract on AssistantMessageItem.
56//
57// Concurrency: glamour's Render is stateful and not safe for
58// concurrent invocation on a shared renderer. Crush's TUI is
59// single-threaded so production never contends, but parallel
60// callers (most notably the test suite) must serialize. We hold
61// [common.LockMarkdownRenderer] for the entire prefix +
62// trailing render sequence so other goroutines cannot interleave
63// their own Render calls and corrupt goldmark's BlockStack.
64func (s *streamingMarkdown) Render(content string, width int, renderer *glamour.TermRenderer) string {
65 mu := common.LockMarkdownRenderer(renderer)
66 mu.Lock()
67 defer mu.Unlock()
68 full := func() string {
69 out, err := renderer.Render(content)
70 if err != nil {
71 return content
72 }
73 return strings.TrimSuffix(out, "\n")
74 }
75
76 // Width change OR content not a prefix-extension: drop cache,
77 // full render, optionally try to seed a fresh boundary on this
78 // call (step "f" in the design note).
79 if width != s.width || !strings.HasPrefix(content, s.stablePrefix) {
80 s.Reset()
81 s.width = width
82 out := full()
83 s.tryAdvanceFromEmpty(content, width, renderer)
84 return out
85 }
86
87 boundary := findSafeMarkdownBoundary(content)
88 if boundary < 0 {
89 // No safe boundary anywhere yet. Full render; do not
90 // modify the cache (a future flush may find one).
91 return full()
92 }
93
94 if boundary <= len(s.stablePrefix) {
95 // Cached prefix already covers an at-least-as-late
96 // boundary. Render the trailing partial fresh and glue.
97 trail := content[len(s.stablePrefix):]
98 return glueRenders(s.stablePrefixRender, s.renderTrailing(trail, renderer))
99 }
100
101 // boundary > len(stablePrefix): we have a NEW chunk of safe
102 // content. Render the new chunk, append to stablePrefixRender,
103 // promote the boundary, then render the remaining trail.
104 newChunk := content[len(s.stablePrefix):boundary]
105 newChunkRender := s.renderTrailing(newChunk, renderer)
106 s.stablePrefixRender = glueRenders(s.stablePrefixRender, newChunkRender)
107 s.stablePrefix = content[:boundary]
108
109 trail := content[boundary:]
110 if trail == "" {
111 // boundary == len(content): no trailing content. Returning
112 // the cached prefix render directly is correct.
113 return s.stablePrefixRender
114 }
115 return glueRenders(s.stablePrefixRender, s.renderTrailing(trail, renderer))
116}
117
118// tryAdvanceFromEmpty seeds the cache from a fresh state. We've
119// already paid the cost of a full render of `content`; if there is
120// a safe boundary inside it, render the prefix once more (cheap
121// relative to the full render we just did) and cache it so the
122// next flush can avoid the full work.
123//
124// This is the optional optimisation step "f" from the design
125// note. We render the prefix separately rather than try to
126// recover it from the full render output because two renders
127// concatenated โ a single render of the whole, and we prefer the
128// cached prefix render to be byte-for-byte what we'd produce on a
129// future cached call.
130func (s *streamingMarkdown) tryAdvanceFromEmpty(content string, width int, renderer *glamour.TermRenderer) {
131 boundary := findSafeMarkdownBoundary(content)
132 if boundary <= 0 {
133 return
134 }
135 prefix := content[:boundary]
136 out, err := renderer.Render(prefix)
137 if err != nil {
138 return
139 }
140 s.stablePrefix = prefix
141 s.stablePrefixRender = trimGlamourMargins(out)
142 s.width = width
143}
144
145// renderTrailing renders a trailing partial as a fresh glamour
146// document and trims the surrounding whitespace so it can be
147// concatenated to a cached prefix render without doubled blank
148// lines.
149func (s *streamingMarkdown) renderTrailing(text string, renderer *glamour.TermRenderer) string {
150 if text == "" {
151 return ""
152 }
153 out, err := renderer.Render(text)
154 if err != nil {
155 return text
156 }
157 return trimGlamourMargins(out)
158}
159
160// glueRenders concatenates two glamour-rendered fragments with a
161// single blank line separator. Glamour outputs typically carry
162// their own surrounding margins; trimming on both sides and
163// gluing with "\n\n" prevents the visible double-margin seam.
164//
165// Empty fragments are tolerated so the same helper works for the
166// "boundary == len(content)" path where there is no trailing
167// segment.
168func glueRenders(prefix, trail string) string {
169 prefix = trimGlamourMargins(prefix)
170 trail = trimGlamourMargins(trail)
171 switch {
172 case prefix == "" && trail == "":
173 return ""
174 case prefix == "":
175 return trail
176 case trail == "":
177 return prefix
178 default:
179 return prefix + "\n\n" + trail
180 }
181}
182
183// trimGlamourMargins strips leading and trailing whitespace
184// (including newlines) from a glamour-rendered fragment.
185// Glamour adds a leading blank line for documents that open with
186// a heading or paragraph, plus a trailing newline; both must be
187// removed before concatenation.
188func trimGlamourMargins(s string) string {
189 return strings.Trim(s, " \t\n")
190}
191
192// findSafeMarkdownBoundary returns the byte offset of the END of
193// the latest safe boundary in content, i.e. the offset such that
194// content[:boundary] is a valid stable-prefix candidate. The
195// returned offset always points immediately after a blank-line
196// separator, so concatenating a fresh render of content[boundary:]
197// to a cached render of content[:boundary] does not require glamour
198// to share state across the cut.
199//
200// Returns -1 when no safe boundary exists. SAFETY FIRST: any time
201// we have the slightest doubt we return -1 and let the caller fall
202// back to a full render.
203//
204// Decision tree, in order of preference (latest boundary wins):
205//
206// 1. Walk backward through every "blank line" position p such that
207// content[:p] ends with "\n\n" (or "\n[ \t]*\n").
208// 2. For each candidate, check that content[:p] has an even
209// number of triple-backtick fence lines (no open fenced
210// block). Any odd count means we'd be cutting inside a fence
211// and mis-syntax-highlighting the trailing partial.
212// 2b. Reject if any line in content[:p] (outside fenced blocks)
213// is a list-marker line, an HTML-block opener, or a link
214// reference definition. See [prefixHasOpenHazard] for the
215// reasoning behind these "anywhere in prefix" rejects.
216// 3. Reject if the last non-blank line of content[:p] is:
217// - a list item marker line ("^\s*([-*+]|\d+\.)\s")
218// - a table line (contains "|")
219// - a block quote ("^\s*>")
220// - a setext header underline ("^=+\s*$" or "^-+\s*$")
221// - an indented code line (4+ leading spaces or a tab)
222// 4. Reject if the line immediately AFTER the boundary (skipping
223// leading blank lines) looks like a setext underline (a line
224// of '=' or '-' only). Rendering the prefix as a paragraph
225// would change once the underline arrived; that's exactly the
226// "splitting changes the prefix render" hazard ยง4.4 calls out.
227//
228// Returns the byte offset of the first character AFTER the blank
229// line, i.e. the start of the trailing segment.
230func findSafeMarkdownBoundary(content string) int {
231 if len(content) == 0 {
232 return -1
233 }
234
235 // Iterate every blank-line position from latest to earliest.
236 for p := blankLineBefore(content, len(content)); p > 0; p = blankLineBefore(content, p-1) {
237 if !isSafeBoundaryAt(content, p) {
238 continue
239 }
240 return p
241 }
242 return -1
243}
244
245// blankLineBefore returns the byte offset of the first character
246// AFTER the latest blank-line separator that ends strictly before
247// `until`. A blank-line separator is a sequence "\n([ \t]*\n)+"
248// โ one newline, then one or more lines containing only spaces or
249// tabs and terminated by another newline. The returned offset is
250// the start of the first non-blank line that follows the
251// separator (or the position immediately after the final newline,
252// if no further content remains).
253//
254// Returns -1 when no blank-line separator exists before `until`.
255func blankLineBefore(content string, until int) int {
256 if until <= 0 {
257 return -1
258 }
259 // Walk backward looking for a newline followed (after optional
260 // blank-line content) by another newline. We track the latest
261 // newline we've seen; if the next earlier newline has only
262 // blank chars between them, we have a blank-line separator
263 // and the boundary sits immediately after the latest newline.
264 end := until
265 for end > 0 {
266 nl := strings.LastIndexByte(content[:end], '\n')
267 if nl < 0 {
268 return -1
269 }
270 // Look for an earlier newline whose gap to nl is empty
271 // or whitespace only.
272 prev := strings.LastIndexByte(content[:nl], '\n')
273 for prev >= 0 {
274 gap := content[prev+1 : nl]
275 if isBlankOrSpaces(gap) {
276 return nl + 1
277 }
278 // Gap had non-whitespace; nl is not a blank-line
279 // separator. Move up: try with the earlier newline as
280 // the new "nl" candidate.
281 break
282 }
283 end = nl
284 }
285 return -1
286}
287
288// isBlankOrSpaces reports whether s consists entirely of spaces
289// and tabs (or is empty).
290func isBlankOrSpaces(s string) bool {
291 for i := range len(s) {
292 if s[i] != ' ' && s[i] != '\t' {
293 return false
294 }
295 }
296 return true
297}
298
299// isSafeBoundaryAt reports whether content[:p] is a safe stable
300// prefix. p must be a blank-line boundary (start of a line, with a
301// blank line immediately preceding).
302//
303// Beyond the last-line checks, three "anywhere in the prefix"
304// hazards force a reject because they cannot be reliably reasoned
305// about by inspecting the trailing line alone. For each of these
306// the simplest, safest rule was chosen โ see prefixHasOpenHazard.
307func isSafeBoundaryAt(content string, p int) bool {
308 prefix := content[:p]
309
310 // (2) Even number of triple-backtick fence lines.
311 if countFenceLines(prefix)%2 != 0 {
312 return false
313 }
314
315 // (2b) Anywhere-in-prefix hazards: open list (B1), HTML block
316 // opener (B2), reference link definition (B3). Any of these
317 // anywhere in the prefix forces a fallback.
318 if prefixHasOpenHazard(prefix) {
319 return false
320 }
321
322 // (3) Inspect the last non-blank line of the prefix.
323 lastLine := lastNonBlankLine(prefix)
324 if lastLine != "" && lineOpensConstruct(lastLine) {
325 return false
326 }
327
328 // (4) If anything follows, make sure it doesn't look like a
329 // setext underline that would retroactively turn the last
330 // paragraph of the prefix into a header.
331 if rest := content[p:]; rest != "" {
332 first := firstNonBlankLine(rest)
333 if isSetextUnderlineCandidate(first) {
334 return false
335 }
336 }
337
338 return true
339}
340
341// prefixHasOpenHazard reports whether prefix contains any of three
342// constructs that cannot be safely cut at a blank-line boundary
343// even when the immediately preceding line looks fine. Each check
344// uses the SIMPLEST viable conservative rule per the F8 round-2
345// review:
346//
347// B1 (loose lists). A loose list has a blank line between an item
348// and a continuation paragraph that begins with indentation
349// but no list marker. If a candidate boundary lands on that
350// blank line, the prefix's trailing non-blank line is the
351// continuation paragraph, NOT a list marker, so the last-line
352// check would accept it even though the list is still open.
353//
354// Rule chosen: any list-marker line ANYWHERE in the prefix
355// forces -1. This is overly conservative โ it forfeits
356// boundary advancement past a closed list โ but it eliminates
357// the entire bug class with zero parsing of CommonMark's
358// loose-list closure semantics. We retain the most useful
359// boundary in practice: the one BEFORE the list opens (no
360// marker has appeared in the prefix yet).
361//
362// B2 (HTML blocks). CommonMark defines seven HTML-block opener
363// patterns (script/pre/style/textarea, comments, processing
364// instructions, CDATA, declarations, recognised tag names).
365// If the prefix opens an HTML block that the suffix closes,
366// splitting renders the prefix as raw HTML and the suffix as
367// prose.
368//
369// Rule chosen: any HTML-block opener anywhere in the prefix
370// forces -1. Same trade-off as B1 โ the typical assistant
371// output contains no raw HTML, so the perf cost is zero in
372// the common case.
373//
374// B3 (reference link definitions). A line of the form
375// "[label]: <url>" defines a link reference that the suffix
376// may later use as "[text][label]". Splitting the document
377// loses the definition because each half is rendered as an
378// independent glamour document.
379//
380// Rule chosen: any reference link definition line anywhere in
381// the prefix forces -1. Suffix-side reference detection is
382// fragile (three syntaxes: [text][label], [label][], [label]),
383// so the prefix-side check is the simpler safe choice.
384//
385// All three rules accept the perf hit of "no boundary after a
386// list / HTML block / link def" in exchange for guaranteed
387// soundness. If profiling shows this kills the F8 win on real
388// streaming traces, the next iteration can promote each rule to
389// its less-conservative variant (closure-aware list tracking,
390// per-tag HTML close detection, suffix-aware ref tracking).
391func prefixHasOpenHazard(prefix string) bool {
392 inFence := false
393 for line := range splitLines(prefix) {
394 // Track fenced state so list/html/ref patterns inside a
395 // fenced code block do not falsely trigger the hazards.
396 if isFenceLine(line) {
397 inFence = !inFence
398 continue
399 }
400 if inFence {
401 continue
402 }
403 trimmed := strings.TrimLeft(line, " \t")
404 if trimmed == "" {
405 continue
406 }
407 // B1: any list-item marker.
408 if isListItemMarker(trimmed) {
409 return true
410 }
411 // B2: HTML block opener.
412 if isHTMLBlockOpener(line) {
413 return true
414 }
415 // B3: link reference definition.
416 if isLinkRefDefinition(line) {
417 return true
418 }
419 }
420 return false
421}
422
423// countFenceLines counts lines that begin a fenced code block in
424// the CommonMark sense: a line whose first non-whitespace run is
425// at least three consecutive backticks (or tildes). Each such
426// line toggles the fenced state, so an even count means every
427// opened fence has been closed.
428//
429// We accept up to three leading spaces of indentation (CommonMark
430// rule) and require the fence characters to be the FIRST
431// non-whitespace content of the line. We deliberately do NOT
432// attempt to parse info-strings or differentiate opener from
433// closer beyond toggling โ a closing fence is just any line
434// whose first non-whitespace run is >=3 of the same fence char.
435func countFenceLines(s string) int {
436 n := 0
437 for line := range splitLines(s) {
438 if isFenceLine(line) {
439 n++
440 }
441 }
442 return n
443}
444
445// isFenceLine reports whether line opens or closes a fenced code
446// block.
447func isFenceLine(line string) bool {
448 // Strip up to 3 spaces of indentation.
449 i := 0
450 for i < len(line) && i < 3 && line[i] == ' ' {
451 i++
452 }
453 if i >= len(line) {
454 return false
455 }
456 c := line[i]
457 if c != '`' && c != '~' {
458 return false
459 }
460 run := 0
461 for i < len(line) && line[i] == c {
462 i++
463 run++
464 }
465 return run >= 3
466}
467
468// lastNonBlankLine returns the last non-blank line of s, or ""
469// when every line is blank.
470func lastNonBlankLine(s string) string {
471 last := ""
472 for line := range splitLines(s) {
473 if strings.TrimSpace(line) != "" {
474 last = line
475 }
476 }
477 return last
478}
479
480// firstNonBlankLine returns the first non-blank line of s, or ""
481// when every line is blank.
482func firstNonBlankLine(s string) string {
483 for line := range splitLines(s) {
484 if strings.TrimSpace(line) != "" {
485 return line
486 }
487 }
488 return ""
489}
490
491// splitLines yields the lines of s without their terminators. The
492// final segment is yielded even if not newline-terminated.
493func splitLines(s string) func(yield func(string) bool) {
494 return func(yield func(string) bool) {
495 start := 0
496 for i := 0; i < len(s); i++ {
497 if s[i] == '\n' {
498 if !yield(s[start:i]) {
499 return
500 }
501 start = i + 1
502 }
503 }
504 if start <= len(s)-1 {
505 yield(s[start:])
506 }
507 }
508}
509
510// lineOpensConstruct reports whether line keeps a markdown
511// construct open across the boundary. We err conservatively โ
512// any case that smells like list/table/quote/setext/indented-code
513// returns true.
514func lineOpensConstruct(line string) bool {
515 // Indented code: a tab, or 4+ leading spaces.
516 if len(line) > 0 && line[0] == '\t' {
517 return true
518 }
519 if strings.HasPrefix(line, " ") {
520 return true
521 }
522
523 trimmed := strings.TrimLeft(line, " \t")
524 if trimmed == "" {
525 return false
526 }
527
528 // Block quote.
529 if trimmed[0] == '>' {
530 return true
531 }
532
533 // List item: "- " "* " "+ " or "<digits>. " or "<digits>) ".
534 if isListItemMarker(trimmed) {
535 return true
536 }
537
538 // Table: any pipe character anywhere in the line. Conservative:
539 // pipe-in-prose is rare and the cost of bailing is one slow
540 // frame.
541 if strings.ContainsRune(line, '|') {
542 return true
543 }
544
545 // Setext underline candidate as the LAST line of the prefix:
546 // this would be a setext header for an even-earlier paragraph.
547 // Refuse to split at all in this case โ the boundary is right
548 // in the middle of a header.
549 if isSetextUnderlineCandidate(trimmed) {
550 return true
551 }
552
553 return false
554}
555
556// isListItemMarker reports whether line (already left-trimmed)
557// starts with a CommonMark list-item marker followed by a space
558// or tab.
559func isListItemMarker(line string) bool {
560 if line == "" {
561 return false
562 }
563 c := line[0]
564 if c == '-' || c == '*' || c == '+' {
565 if len(line) >= 2 && (line[1] == ' ' || line[1] == '\t') {
566 return true
567 }
568 return false
569 }
570 // Ordered list: digits followed by '.' or ')' and a space.
571 i := 0
572 for i < len(line) && line[i] >= '0' && line[i] <= '9' {
573 i++
574 }
575 if i == 0 || i > 9 {
576 return false
577 }
578 if i >= len(line) {
579 return false
580 }
581 if line[i] != '.' && line[i] != ')' {
582 return false
583 }
584 if i+1 >= len(line) {
585 return false
586 }
587 return line[i+1] == ' ' || line[i+1] == '\t'
588}
589
590// isSetextUnderlineCandidate reports whether line (with optional
591// leading whitespace) consists entirely of '=' or entirely of '-'
592// characters with optional trailing whitespace. CommonMark
593// requires no leading whitespace on the underline; we accept up
594// to three spaces for safety so an indented underline still
595// blocks a split.
596func isSetextUnderlineCandidate(line string) bool {
597 // Strip leading whitespace.
598 i := 0
599 for i < len(line) && (line[i] == ' ' || line[i] == '\t') {
600 i++
601 }
602 if i == len(line) {
603 return false
604 }
605 c := line[i]
606 if c != '=' && c != '-' {
607 return false
608 }
609 j := i
610 for j < len(line) && line[j] == c {
611 j++
612 }
613 // Allow trailing whitespace.
614 for j < len(line) {
615 if line[j] != ' ' && line[j] != '\t' {
616 return false
617 }
618 j++
619 }
620 // Need at least one underline character. "-" alone is also a
621 // list marker without a trailing space; the listItem check
622 // covers the marker case before we get here.
623 return j-i >= 1
624}
625
626// isHTMLBlockOpener reports whether line begins one of the seven
627// CommonMark HTML block patterns. We accept up to three spaces of
628// leading indentation (CommonMark rule). Matching is intentionally
629// loose โ we only need to know the line "looks like an HTML
630// block start", not parse the contained markup.
631func isHTMLBlockOpener(line string) bool {
632 // Strip up to 3 spaces of indentation.
633 i := 0
634 for i < len(line) && i < 3 && line[i] == ' ' {
635 i++
636 }
637 rest := line[i:]
638 if len(rest) < 2 || rest[0] != '<' {
639 return false
640 }
641
642 // Type 2: HTML comment "<!--".
643 if strings.HasPrefix(rest, "<!--") {
644 return true
645 }
646 // Type 3: processing instruction "<?".
647 if strings.HasPrefix(rest, "<?") {
648 return true
649 }
650 // Type 5: CDATA "<![CDATA[".
651 if strings.HasPrefix(rest, "<![CDATA[") {
652 return true
653 }
654 // Type 4: declaration "<!" followed by an ASCII letter.
655 if len(rest) >= 3 && rest[1] == '!' && isASCIILetter(rest[2]) {
656 return true
657 }
658
659 // Type 1: <script | <pre | <style | <textarea (case-insensitive)
660 // followed by whitespace, '>', end-of-line, or other non-name
661 // terminators. Use a permissive HasPrefix check on lowercase.
662 low := strings.ToLower(rest)
663 for _, t := range []string{"<script", "<pre", "<style", "<textarea"} {
664 if strings.HasPrefix(low, t) {
665 next := byte(0)
666 if len(low) > len(t) {
667 next = low[len(t)]
668 }
669 if next == 0 || next == ' ' || next == '\t' || next == '>' {
670 return true
671 }
672 }
673 }
674
675 // Types 6 & 7: open or close of a block-level tag.
676 //
677 // Type 6 matches a fixed CommonMark tag set; type 7 matches any
678 // otherwise-valid open/close tag whose name is not in the
679 // script/pre/style/textarea family. We collapse both into a
680 // single check: the line must start with '<' or '</' followed
681 // by an ASCII letter. This deliberately mirrors the other
682 // hazards โ when in doubt, forfeit the boundary. Lines like
683 // "<3", "<-", "<<", or mid-line "<foo>" do NOT trigger because
684 // we require the line to *start* (after up to 3 spaces) with
685 // '<letter' or '</letter'.
686 j := 1 // past '<'
687 if j < len(rest) && rest[j] == '/' {
688 j++
689 }
690 if j >= len(rest) || !isASCIILetter(rest[j]) {
691 return false
692 }
693 return true
694}
695
696// isASCIILetter reports whether b is an ASCII letter.
697func isASCIILetter(b byte) bool {
698 return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
699}
700
701// isLinkRefDefinition reports whether line matches a CommonMark
702// link reference definition opener. The conservative pattern:
703//
704// ^[ ]{0,3}\[[^\]]+\]:\s*\S+
705//
706// i.e. up to 3 spaces, then a bracketed label (no nested ']'),
707// then a colon, then whitespace, then at least one non-whitespace
708// character of destination. We do not validate the destination โ
709// presence of a ref-def opener anywhere in the prefix is enough
710// to forfeit the boundary.
711func isLinkRefDefinition(line string) bool {
712 i := 0
713 for i < len(line) && i < 3 && line[i] == ' ' {
714 i++
715 }
716 if i >= len(line) || line[i] != '[' {
717 return false
718 }
719 i++
720 labelStart := i
721 for i < len(line) && line[i] != ']' {
722 i++
723 }
724 if i >= len(line) || i == labelStart {
725 // No closing bracket, or empty label.
726 return false
727 }
728 // i points at ']'.
729 i++
730 if i >= len(line) || line[i] != ':' {
731 return false
732 }
733 i++
734 // Skip required whitespace.
735 for i < len(line) && (line[i] == ' ' || line[i] == '\t') {
736 i++
737 }
738 // At least one non-whitespace character of destination.
739 return i < len(line)
740}