turbo-editors/turbo-corepublic Fork 0
v1.0.2
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.

📦 Turbo Core f3ade8d · on v1.0.2 · k33g · 8h ago
blocks.go · 118 lines · 3.4 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
// Fenced code blocks: what an agent writes in Markdown, and the one place a
// scanner can be pointed at with confidence.

package acp

import (
	"strings"

	"rickub.com/turbo-editors/turbo-core/syntax"
)

// Block is a run of an entry's text that is either prose or code.
//
// An agent answers in Markdown, so a fenced block is a real thing in what it
// sends and the one place a scanner can be pointed at with confidence.
type Block struct {
	// Language is what the fence named, or LanguageNone for prose and for a
	// fence that named nothing.
	Language syntax.Language
	// Code says this block came out of a fence.
	Code bool
	// Text is the block's own text, without the fence markers.
	Text string
}

// SplitBlocks separates fenced code from the prose around it.
//
// Only the *fence* decides. A scanner is never guessed at from the shape of
// the text: a highlighter that is wrong is worse than one that is quiet, which
// is the rule every scanner in this library already follows.
//
//	blocks := acp.SplitBlocks("here:\n```go\nx := 1\n```\n")
//	blocks[1].Language // "go"
func SplitBlocks(text string) []Block {
	var blocks []Block
	var current []string
	inFence, language := false, syntax.LanguageNone

	flush := func(code bool, lang syntax.Language) {
		if len(current) == 0 {
			return
		}
		blocks = append(blocks, Block{Language: lang, Code: code, Text: strings.Join(current, "\n")})
		current = nil
	}

	for _, line := range strings.Split(text, "\n") {
		if fence, named := fenceOf(line); fence {
			if inFence {
				flush(true, language)
				inFence, language = false, syntax.LanguageNone
				continue
			}
			flush(false, syntax.LanguageNone)
			inFence, language = true, named
			continue
		}
		current = append(current, line)
	}

	// A fence nobody closed is still code: an agent cut off mid-answer leaves
	// one, and drawing the rest as prose would change colour halfway down a
	// function for no reason a reader can see.
	flush(inFence, language)
	return blocks
}

// fenceOf reports whether a line opens or closes a fence, and which language
// it named.
func fenceOf(line string) (bool, syntax.Language) {
	trimmed := strings.TrimSpace(line)
	if !strings.HasPrefix(trimmed, "```") {
		return false, syntax.LanguageNone
	}

	name := strings.ToLower(strings.TrimSpace(strings.TrimPrefix(trimmed, "```")))
	if name == "" {
		return true, syntax.LanguageNone
	}
	return true, languageNamed(name)
}

// languageNamed returns the registered language a fence's tag means, or
// LanguageNone when no scanner claims it.
//
// The tag is matched against what the editor actually knows rather than
// against a list kept here, so an editor that registers Rust colours a ```rust
// block with no change to this file. The few aliases below are the ones a
// model reaches for by habit.
func languageNamed(name string) syntax.Language {
	if alias, ok := fenceAliases[name]; ok {
		name = alias
	}

	wanted := syntax.Language(name)
	for _, known := range syntax.Registered() {
		if known == wanted {
			return known
		}
	}
	return syntax.LanguageNone
}

// fenceAliases maps the tags models write onto the names languages register
// themselves under.
var fenceAliases = map[string]string{
	"golang":        "go",
	"sh":            "bash",
	"shell":         "bash",
	"zsh":           "bash",
	"console":       "bash",
	"js":            "javascript",
	"mjs":           "javascript",
	"yml":           "yaml",
	"md":            "markdown",
	"docker":        "dockerfile",
	"containerfile": "dockerfile",
}