summaryrefslogtreecommitdiff
path: root/internal/textutil
diff options
context:
space:
mode:
Diffstat (limited to 'internal/textutil')
-rw-r--r--internal/textutil/textutil.go192
-rw-r--r--internal/textutil/textutil_test.go130
2 files changed, 169 insertions, 153 deletions
diff --git a/internal/textutil/textutil.go b/internal/textutil/textutil.go
index 7ef2680..1e9da3c 100644
--- a/internal/textutil/textutil.go
+++ b/internal/textutil/textutil.go
@@ -4,111 +4,125 @@ import "strings"
// RenderTemplate performs simple {{var}} replacement in a template string.
func RenderTemplate(t string, vars map[string]string) string {
- if t == "" || len(vars) == 0 {
- return t
- }
- out := t
- for k, v := range vars {
- out = strings.ReplaceAll(out, "{{"+k+"}}", v)
- }
- return out
+ if t == "" || len(vars) == 0 {
+ return t
+ }
+ out := t
+ for k, v := range vars {
+ out = strings.ReplaceAll(out, "{{"+k+"}}", v)
+ }
+ return out
}
// StripCodeFences removes surrounding Markdown triple-backtick fences.
func StripCodeFences(s string) string {
- t := strings.TrimSpace(s)
- if t == "" {
- return t
- }
- lines := strings.Split(t, "\n")
- start := 0
- for start < len(lines) && strings.TrimSpace(lines[start]) == "" {
- start++
- }
- end := len(lines) - 1
- for end >= 0 && strings.TrimSpace(lines[end]) == "" {
- end--
- }
- if start >= len(lines) || end < 0 || start > end {
- return t
- }
- first := strings.TrimSpace(lines[start])
- last := strings.TrimSpace(lines[end])
- if strings.HasPrefix(first, "```") && last == "```" && end > start {
- inner := strings.Join(lines[start+1:end], "\n")
- return inner
- }
- return t
+ t := strings.TrimSpace(s)
+ if t == "" {
+ return t
+ }
+ lines := strings.Split(t, "\n")
+ start := 0
+ for start < len(lines) && strings.TrimSpace(lines[start]) == "" {
+ start++
+ }
+ end := len(lines) - 1
+ for end >= 0 && strings.TrimSpace(lines[end]) == "" {
+ end--
+ }
+ if start >= len(lines) || end < 0 || start > end {
+ return t
+ }
+ first := strings.TrimSpace(lines[start])
+ last := strings.TrimSpace(lines[end])
+ if strings.HasPrefix(first, "```") && last == "```" && end > start {
+ inner := strings.Join(lines[start+1:end], "\n")
+ return inner
+ }
+ return t
}
// InstructionFromSelection extracts the first inline instruction and returns
// (instruction, cleanedSelection). It detects markers on the earliest position
// per line in precedence: strict ;text;, /* */, <!-- -->, //, #, --.
func InstructionFromSelection(sel string) (string, string) {
- lines := strings.Split(sel, "\n")
- for idx, line := range lines {
- if instr, cleaned, ok := FindFirstInstructionInLine(line); ok && strings.TrimSpace(instr) != "" {
- lines[idx] = cleaned
- return instr, strings.Join(lines, "\n")
- }
- }
- return "", sel
+ lines := strings.Split(sel, "\n")
+ for idx, line := range lines {
+ if instr, cleaned, ok := FindFirstInstructionInLine(line); ok && strings.TrimSpace(instr) != "" {
+ lines[idx] = cleaned
+ return instr, strings.Join(lines, "\n")
+ }
+ }
+ return "", sel
}
// FindFirstInstructionInLine returns (instruction, cleaned, ok) for a single line.
func FindFirstInstructionInLine(line string) (instr, cleaned string, ok bool) {
- type cand struct{ start, end int; text string }
- cands := []cand{}
- if t, l, r, ok := FindStrictInlineTag(line); ok {
- cands = append(cands, cand{start: l, end: r, text: t})
- }
- if i := strings.Index(line, "/*"); i >= 0 {
- if j := strings.Index(line[i+2:], "*/"); j >= 0 {
- start := i
- end := i + 2 + j + 2
- text := strings.TrimSpace(line[i+2 : i+2+j])
- cands = append(cands, cand{start: start, end: end, text: text})
- }
- }
- if i := strings.Index(line, "<!--"); i >= 0 {
- if j := strings.Index(line[i+4:], "-->"); j >= 0 {
- start := i
- end := i + 4 + j + 3
- text := strings.TrimSpace(line[i+4 : i+4+j])
- cands = append(cands, cand{start: start, end: end, text: text})
- }
- }
- if i := strings.Index(line, "//"); i >= 0 {
- cands = append(cands, cand{start: i, end: len(line), text: strings.TrimSpace(line[i+2:])})
- }
- if i := strings.Index(line, "#"); i >= 0 {
- cands = append(cands, cand{start: i, end: len(line), text: strings.TrimSpace(line[i+1:])})
- }
- if i := strings.Index(line, "--"); i >= 0 {
- cands = append(cands, cand{start: i, end: len(line), text: strings.TrimSpace(line[i+2:])})
- }
- if len(cands) == 0 { return "", line, false }
- best := cands[0]
- for _, c := range cands[1:] {
- if c.start >= 0 && (best.start < 0 || c.start < best.start) { best = c }
- }
- cleaned = strings.TrimRight(line[:best.start]+line[best.end:], " \t")
- return best.text, cleaned, true
+ type cand struct {
+ start, end int
+ text string
+ }
+ cands := []cand{}
+ if t, l, r, ok := FindStrictInlineTag(line); ok {
+ cands = append(cands, cand{start: l, end: r, text: t})
+ }
+ if i := strings.Index(line, "/*"); i >= 0 {
+ if j := strings.Index(line[i+2:], "*/"); j >= 0 {
+ start := i
+ end := i + 2 + j + 2
+ text := strings.TrimSpace(line[i+2 : i+2+j])
+ cands = append(cands, cand{start: start, end: end, text: text})
+ }
+ }
+ if i := strings.Index(line, "<!--"); i >= 0 {
+ if j := strings.Index(line[i+4:], "-->"); j >= 0 {
+ start := i
+ end := i + 4 + j + 3
+ text := strings.TrimSpace(line[i+4 : i+4+j])
+ cands = append(cands, cand{start: start, end: end, text: text})
+ }
+ }
+ if i := strings.Index(line, "//"); i >= 0 {
+ cands = append(cands, cand{start: i, end: len(line), text: strings.TrimSpace(line[i+2:])})
+ }
+ if i := strings.Index(line, "#"); i >= 0 {
+ cands = append(cands, cand{start: i, end: len(line), text: strings.TrimSpace(line[i+1:])})
+ }
+ if i := strings.Index(line, "--"); i >= 0 {
+ cands = append(cands, cand{start: i, end: len(line), text: strings.TrimSpace(line[i+2:])})
+ }
+ if len(cands) == 0 {
+ return "", line, false
+ }
+ best := cands[0]
+ for _, c := range cands[1:] {
+ if c.start >= 0 && (best.start < 0 || c.start < best.start) {
+ best = c
+ }
+ }
+ cleaned = strings.TrimRight(line[:best.start]+line[best.end:], " \t")
+ return best.text, cleaned, true
}
// FindStrictInlineTag finds ;text; with no spaces after/before semicolons.
func FindStrictInlineTag(line string) (text string, left, right int, ok bool) {
- for i := 0; i < len(line); i++ {
- if line[i] != ';' { continue }
- if i+1 < len(line) && line[i+1] == ' ' { continue }
- for j := i + 1; j < len(line); j++ {
- if line[j] == ';' {
- if j-1 >= 0 && line[j-1] == ' ' { continue }
- inner := strings.TrimSpace(line[i+1 : j])
- if inner != "" { return inner, i, j + 1, true }
- }
- }
- }
- return "", -1, -1, false
+ for i := 0; i < len(line); i++ {
+ if line[i] != ';' {
+ continue
+ }
+ if i+1 < len(line) && line[i+1] == ' ' {
+ continue
+ }
+ for j := i + 1; j < len(line); j++ {
+ if line[j] == ';' {
+ if j-1 >= 0 && line[j-1] == ' ' {
+ continue
+ }
+ inner := strings.TrimSpace(line[i+1 : j])
+ if inner != "" {
+ return inner, i, j + 1, true
+ }
+ }
+ }
+ }
+ return "", -1, -1, false
}
-
diff --git a/internal/textutil/textutil_test.go b/internal/textutil/textutil_test.go
index 3a8cd90..cfe8de8 100644
--- a/internal/textutil/textutil_test.go
+++ b/internal/textutil/textutil_test.go
@@ -1,87 +1,89 @@
package textutil
import (
- "regexp"
- "strings"
- "testing"
+ "regexp"
+ "strings"
+ "testing"
)
func TestRenderTemplate_Basic(t *testing.T) {
- out := RenderTemplate("Hello, {{name}}!", map[string]string{"name": "Hex"})
- if out != "Hello, Hex!" {
- t.Fatalf("render failed: %q", out)
- }
- // No vars
- if RenderTemplate("x", nil) != "x" { t.Fatal("nil vars changed output") }
+ out := RenderTemplate("Hello, {{name}}!", map[string]string{"name": "Hex"})
+ if out != "Hello, Hex!" {
+ t.Fatalf("render failed: %q", out)
+ }
+ // No vars
+ if RenderTemplate("x", nil) != "x" {
+ t.Fatal("nil vars changed output")
+ }
}
func TestStripCodeFences_Variants(t *testing.T) {
- cases := []struct{ in, want string }{
- {"```\ncode\n```", "code"},
- {"```go\npackage x\n```", "package x"},
- {"no fences", "no fences"},
- {"\n\n```\ntrim\n```\n", "trim"},
- }
- for _, c := range cases {
- if got := StripCodeFences(c.in); got != c.want {
- t.Fatalf("strip mismatch: %q != %q", got, c.want)
- }
- }
+ cases := []struct{ in, want string }{
+ {"```\ncode\n```", "code"},
+ {"```go\npackage x\n```", "package x"},
+ {"no fences", "no fences"},
+ {"\n\n```\ntrim\n```\n", "trim"},
+ }
+ for _, c := range cases {
+ if got := StripCodeFences(c.in); got != c.want {
+ t.Fatalf("strip mismatch: %q != %q", got, c.want)
+ }
+ }
}
func TestInstructionFromSelection_Markers(t *testing.T) {
- inputs := []string{
- ";do it;\ncode",
- "/* fix */\ncode",
- "<!-- doc -->\ncode",
- "// change\ncode",
- "# tweak\ncode",
- "-- op\ncode",
- }
- for _, in := range inputs {
- instr, cleaned := InstructionFromSelection(in)
- if strings.TrimSpace(instr) == "" {
- t.Fatalf("no instruction for input: %q", in)
- }
- // cleaned should not contain the instruction token
- if strings.Contains(cleaned, instr) {
- // Allow coincidence only if separated differently; require not exact match on same line
- first := strings.Split(in, "\n")[0]
- if strings.Contains(first, instr) {
- t.Fatalf("instruction not removed: %q", cleaned)
- }
- }
- }
+ inputs := []string{
+ ";do it;\ncode",
+ "/* fix */\ncode",
+ "<!-- doc -->\ncode",
+ "// change\ncode",
+ "# tweak\ncode",
+ "-- op\ncode",
+ }
+ for _, in := range inputs {
+ instr, cleaned := InstructionFromSelection(in)
+ if strings.TrimSpace(instr) == "" {
+ t.Fatalf("no instruction for input: %q", in)
+ }
+ // cleaned should not contain the instruction token
+ if strings.Contains(cleaned, instr) {
+ // Allow coincidence only if separated differently; require not exact match on same line
+ first := strings.Split(in, "\n")[0]
+ if strings.Contains(first, instr) {
+ t.Fatalf("instruction not removed: %q", cleaned)
+ }
+ }
+ }
}
func TestFindFirstInstructionInLine_EarliestWins(t *testing.T) {
- // Both markers present, earliest should win (strict tag first)
- line := ";first; // later"
- instr, cleaned, ok := FindFirstInstructionInLine(line)
- if !ok || instr != "first" {
- t.Fatalf("expected 'first', got %q ok=%v", instr, ok)
- }
- if strings.Contains(cleaned, instr) {
- t.Fatalf("expected cleaned line to remove instr: %q", cleaned)
- }
+ // Both markers present, earliest should win (strict tag first)
+ line := ";first; // later"
+ instr, cleaned, ok := FindFirstInstructionInLine(line)
+ if !ok || instr != "first" {
+ t.Fatalf("expected 'first', got %q ok=%v", instr, ok)
+ }
+ if strings.Contains(cleaned, instr) {
+ t.Fatalf("expected cleaned line to remove instr: %q", cleaned)
+ }
}
func TestFindStrictInlineTag(t *testing.T) {
- if txt, l, r, ok := FindStrictInlineTag("pre;do;post"); !ok || txt != "do" || l != 3 || r != 7 {
- t.Fatalf("strict tag parse failed: %q %d %d %v", txt, l, r, ok)
- }
- if _, _, _, ok := FindStrictInlineTag("; spaced ;"); ok {
- t.Fatalf("should reject spaced strict tag")
- }
+ if txt, l, r, ok := FindStrictInlineTag("pre;do;post"); !ok || txt != "do" || l != 3 || r != 7 {
+ t.Fatalf("strict tag parse failed: %q %d %d %v", txt, l, r, ok)
+ }
+ if _, _, _, ok := FindStrictInlineTag("; spaced ;"); ok {
+ t.Fatalf("should reject spaced strict tag")
+ }
}
// optional: ensure no ANSI codes appear in plain helpers
func TestNoANSIInHelpers(t *testing.T) {
- ansi := regexp.MustCompile(`\x1b\[[0-9;]*m`)
- if ansi.MatchString(RenderTemplate("x", nil)) {
- t.Fatalf("unexpected ansi in RenderTemplate")
- }
- if ansi.MatchString(StripCodeFences("x")) {
- t.Fatalf("unexpected ansi in StripCodeFences")
- }
+ ansi := regexp.MustCompile(`\x1b\[[0-9;]*m`)
+ if ansi.MatchString(RenderTemplate("x", nil)) {
+ t.Fatalf("unexpected ansi in RenderTemplate")
+ }
+ if ansi.MatchString(StripCodeFences("x")) {
+ t.Fatalf("unexpected ansi in StripCodeFences")
+ }
}