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.

blocks.go · 118 lines · 3.4 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 19h ago1// Fenced code blocks: what an agent writes in Markdown, and the one place a
2// scanner can be pointed at with confidence.
3
4package acp
5
6import (
7 "strings"
8
📦 Turbo Core f3ade8d k33g 12h ago9 "rickub.com/turbo-editors/turbo-core/syntax"
🛟 Updated. 28d5985 k33g 19h ago10)
11
12// Block is a run of an entry's text that is either prose or code.
13//
14// An agent answers in Markdown, so a fenced block is a real thing in what it
15// sends and the one place a scanner can be pointed at with confidence.
16type Block struct {
17 // Language is what the fence named, or LanguageNone for prose and for a
18 // fence that named nothing.
19 Language syntax.Language
20 // Code says this block came out of a fence.
21 Code bool
22 // Text is the block's own text, without the fence markers.
23 Text string
24}
25
26// SplitBlocks separates fenced code from the prose around it.
27//
28// Only the *fence* decides. A scanner is never guessed at from the shape of
29// the text: a highlighter that is wrong is worse than one that is quiet, which
30// is the rule every scanner in this library already follows.
31//
32// blocks := acp.SplitBlocks("here:\n```go\nx := 1\n```\n")
33// blocks[1].Language // "go"
34func SplitBlocks(text string) []Block {
35 var blocks []Block
36 var current []string
37 inFence, language := false, syntax.LanguageNone
38
39 flush := func(code bool, lang syntax.Language) {
40 if len(current) == 0 {
41 return
42 }
43 blocks = append(blocks, Block{Language: lang, Code: code, Text: strings.Join(current, "\n")})
44 current = nil
45 }
46
47 for _, line := range strings.Split(text, "\n") {
48 if fence, named := fenceOf(line); fence {
49 if inFence {
50 flush(true, language)
51 inFence, language = false, syntax.LanguageNone
52 continue
53 }
54 flush(false, syntax.LanguageNone)
55 inFence, language = true, named
56 continue
57 }
58 current = append(current, line)
59 }
60
61 // A fence nobody closed is still code: an agent cut off mid-answer leaves
62 // one, and drawing the rest as prose would change colour halfway down a
63 // function for no reason a reader can see.
64 flush(inFence, language)
65 return blocks
66}
67
68// fenceOf reports whether a line opens or closes a fence, and which language
69// it named.
70func fenceOf(line string) (bool, syntax.Language) {
71 trimmed := strings.TrimSpace(line)
72 if !strings.HasPrefix(trimmed, "```") {
73 return false, syntax.LanguageNone
74 }
75
76 name := strings.ToLower(strings.TrimSpace(strings.TrimPrefix(trimmed, "```")))
77 if name == "" {
78 return true, syntax.LanguageNone
79 }
80 return true, languageNamed(name)
81}
82
83// languageNamed returns the registered language a fence's tag means, or
84// LanguageNone when no scanner claims it.
85//
86// The tag is matched against what the editor actually knows rather than
87// against a list kept here, so an editor that registers Rust colours a ```rust
88// block with no change to this file. The few aliases below are the ones a
89// model reaches for by habit.
90func languageNamed(name string) syntax.Language {
91 if alias, ok := fenceAliases[name]; ok {
92 name = alias
93 }
94
95 wanted := syntax.Language(name)
96 for _, known := range syntax.Registered() {
97 if known == wanted {
98 return known
99 }
100 }
101 return syntax.LanguageNone
102}
103
104// fenceAliases maps the tags models write onto the names languages register
105// themselves under.
106var fenceAliases = map[string]string{
107 "golang": "go",
108 "sh": "bash",
109 "shell": "bash",
110 "zsh": "bash",
111 "console": "bash",
112 "js": "javascript",
113 "mjs": "javascript",
114 "yml": "yaml",
115 "md": "markdown",
116 "docker": "dockerfile",
117 "containerfile": "dockerfile",
118}