diff options
| author | Paul Buetow <paul@buetow.org> | 2025-09-08 12:02:40 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-09-08 12:02:40 +0300 |
| commit | 75cf6abd55bfb60324fc47cf91eac08dbb8b87b4 (patch) | |
| tree | 6ef90d8014fe4d9a757d3f7e95bf736b70e4c685 /internal/textutil | |
| parent | 0dcf347c3fbc6e4ffb7e46294f5dd92dbbcd98ef (diff) | |
docs: move tmux documentation to its own file
Diffstat (limited to 'internal/textutil')
| -rw-r--r-- | internal/textutil/textutil.go | 192 | ||||
| -rw-r--r-- | internal/textutil/textutil_test.go | 130 |
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") + } } |
