turbo-editors/turbo-corepublic Fork 0
v1.0.0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

🛟 Updated. 28d5985 · on v1.0.0 · k33g · 15h ago
markdown.go · 159 lines · 4.2 KBGo Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
package syntax

// highlightMarkdown colours a Markdown document, one slice of spans per line.
//
// Markdown is scanned a line at a time because almost all of it is decided by
// a line's own shape: what starts it says whether it is a heading, a list item
// or a quotation. The exception is the fenced code block, which is what the
// carried state is for.
func highlightMarkdown(src string) [][]Span {
	return ScanLines(src, scanMarkdownLine)
}

// fence is the state carried between lines: the run of characters that will
// close an open code block, or "" when none is open.
type fence string

// fenceMinimum is how many backticks or tildes open a code block.
const fenceMinimum = 3

// scanMarkdownLine returns the spans of one line, and what is left open.
func scanMarkdownLine(line []rune, open fence) ([]Span, fence) {
	s := &LineScanner{line: line}

	if open != "" {
		return closeOrContinueFence(s, open)
	}
	if marker, ok := fenceOpener(line); ok {
		// The whole opening line, language tag and all, reads as the block.
		s.TakeRest(ClassString)
		return s.spans, marker
	}

	scanMarkdownBlock(s)
	return s.spans, ""
}

// closeOrContinueFence colours a line inside a code block, and says whether
// the block ended on it.
//
// A fenced block is one colour whatever language it announces. Colouring the
// contents with that language's scanner would be better and is a feature of
// its own: it means every scanner has to be reachable from every other, and
// the columns of the inner result have to be mapped back out.
func closeOrContinueFence(s *LineScanner, open fence) ([]Span, fence) {
	s.TakeRest(ClassString)
	if closesFence(s.line, open) {
		return s.spans, ""
	}
	return s.spans, open
}

// fenceOpener reports whether a line opens a code block, and with what.
func fenceOpener(line []rune) (fence, bool) {
	trimmed := skipLeadingSpaces(line)
	if len(trimmed) < fenceMinimum {
		return "", false
	}

	marker := trimmed[0]
	if marker != '`' && marker != '~' {
		return "", false
	}
	for i := range fenceMinimum {
		if trimmed[i] != marker {
			return "", false
		}
	}
	return fence(string(marker)), true
}

// closesFence reports whether a line is the closing run of an open block.
//
// A closing fence is a run of the same character and nothing else, which is
// what keeps a line of prose containing backticks from ending the block.
func closesFence(line []rune, open fence) bool {
	trimmed := skipLeadingSpaces(line)
	marker := []rune(string(open))[0]

	count := 0
	for count < len(trimmed) && trimmed[count] == marker {
		count++
	}
	if count < fenceMinimum {
		return false
	}
	for _, r := range trimmed[count:] {
		if r != ' ' && r != '\t' {
			return false
		}
	}
	return true
}

// skipLeadingSpaces returns a line without its indentation.
func skipLeadingSpaces(line []rune) []rune {
	for i, r := range line {
		if r != ' ' && r != '\t' {
			return line[i:]
		}
	}
	return nil
}

// scanMarkdownBlock colours one line that is not inside a code block.
func scanMarkdownBlock(s *LineScanner) {
	s.SkipSpaces()

	switch {
	case s.Peek(0) == '#':
		s.TakeRest(ClassHeading) // a heading is coloured whole, marker included
	case isHorizontalRule(s.line):
		s.TakeRest(ClassPunctuation)
	case s.Peek(0) == '>':
		s.Take(1, ClassPunctuation)
		scanMarkdownInline(s)
	default:
		takeListMarker(s)
		scanMarkdownInline(s)
	}
}

// isHorizontalRule reports whether a line is three or more of -, * or _ with
// nothing else on it.
//
// It is checked before the list marker, because "---" is a rule and "- item"
// is a list, and they start the same way.
func isHorizontalRule(line []rune) bool {
	trimmed := skipLeadingSpaces(line)
	if len(trimmed) < fenceMinimum {
		return false
	}

	marker := trimmed[0]
	if marker != '-' && marker != '*' && marker != '_' {
		return false
	}
	for _, r := range trimmed {
		if r != marker && r != ' ' && r != '\t' {
			return false
		}
	}
	return true
}

// takeListMarker colours the bullet or the number that starts a list item.
func takeListMarker(s *LineScanner) {
	if (s.Peek(0) == '-' || s.Peek(0) == '*' || s.Peek(0) == '+') && s.Peek(1) == ' ' {
		s.Take(1, ClassPunctuation)
		return
	}

	digits := 0
	for IsDigit(s.Peek(digits)) {
		digits++
	}
	if digits > 0 && (s.Peek(digits) == '.' || s.Peek(digits) == ')') && s.Peek(digits+1) == ' ' {
		s.Take(digits+1, ClassPunctuation)
	}
}