mirror of
https://github.com/lukaszraczylo/filepuff-mcp.git
synced 2026-10-15 05:37:37 +00:00
V2/token optimization (#11)
* v2.0: token-optimization overhaul Additive (backward-compatible flags): - file_read: skeleton mode, strip (imports/license/block_comments), compact_line_numbers, 8-char etag with prefix-match compat - ast_query: format=verbose|compact|location, pagination cursor - file_search: cluster mode, pagination cursor - lsp_query (references): compact output Breaking (v2): - Preambles removed; opt-in verbose=true restores - edit_apply: response=count|diff|none, default count - ping tool removed - symbol_at/find_definition/find_references merged into lsp_query - Tool descriptions trimmed -83%, help moved to filepuff://help/<tool> - Batch file_read dedups by etag Protocol: - ResourceLink returned for file_read >64 KiB (force_inline override) - OnAfterInitialize hook reads capabilities.experimental.filepuff for session defaults (default_format, default_max_results, default_cluster, compact_refs, line_numbers, resource_link_threshold) * fix: drop --max-total-count from ripgrep args The flag does not exist in stable ripgrep (confirmed up to 15.1.0 -- "unrecognized flag --max-total-count, similar flags that are available: --max-count"). Every file_search call failed on hosts with stock rg. --max-count is per-file, not a drop-in replacement, so rely on the in-process truncation in parseOutput that was already the documented safety net.
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
// Package parser provides skeleton rendering for source files.
|
||||
package parser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/lukaszraczylo/mcp-filepuff/pkg/protocol"
|
||||
sitter "github.com/smacker/go-tree-sitter"
|
||||
)
|
||||
|
||||
// SkeletonFile returns a skeleton representation of the file: top-level declarations
|
||||
// with signatures and doc-comments intact, but function/method bodies replaced with
|
||||
// a language-appropriate placeholder.
|
||||
//
|
||||
// Supported: go, typescript, javascript, python, rust.
|
||||
// Other languages fall back to symbols_only (AST summary text).
|
||||
// Returns (skeletonText, isFullSkeleton, error).
|
||||
func SkeletonFile(ctx context.Context, reg *Registry, filename string, content []byte) (string, bool, error) {
|
||||
result, err := reg.Parse(ctx, filename, content)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
lang := protocol.DetectLanguage(filename)
|
||||
|
||||
switch lang {
|
||||
case protocol.LangGo:
|
||||
return skeletonGo(result.Tree, content), true, nil
|
||||
case protocol.LangTypeScript, protocol.LangJavaScript:
|
||||
return skeletonTS(result.Tree, content), true, nil
|
||||
case protocol.LangPython:
|
||||
return skeletonPython(result.Tree, content), true, nil
|
||||
case protocol.LangRust:
|
||||
return skeletonRust(result.Tree, content), true, nil
|
||||
default:
|
||||
// TODO: skeleton for c, cpp, elixir, html, vue — fall back to symbols_only
|
||||
syms := ExtractSymbols(result.Tree, content, lang, filename)
|
||||
return renderSymbolsOnly(syms, filename, lang, content), false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// renderSymbolsOnly renders a simple symbol list (fallback for unsupported languages).
|
||||
func renderSymbolsOnly(syms []protocol.Symbol, _ string, lang protocol.Language, content []byte) string {
|
||||
lines := strings.Split(string(content), "\n")
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("// skeleton unavailable for %s — symbol list only\n", lang))
|
||||
for _, s := range syms {
|
||||
sb.WriteString(fmt.Sprintf("// %s %s (line %d)\n", s.Kind, s.Name, s.Location.Line))
|
||||
if s.Doc != "" {
|
||||
sb.WriteString(fmt.Sprintf("// doc: %s\n", s.Doc))
|
||||
}
|
||||
if s.Location.Line >= 1 && s.Location.Line <= len(lines) {
|
||||
sb.WriteString(lines[s.Location.Line-1] + " { ... }\n")
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// ---- Go skeleton ----
|
||||
|
||||
// skeletonGoBodyNodes lists Go node types that have a body field to replace.
|
||||
var skeletonGoBodyNodes = map[string]string{
|
||||
"function_declaration": "body",
|
||||
"method_declaration": "body",
|
||||
}
|
||||
|
||||
func skeletonGo(tree *sitter.Tree, content []byte) string {
|
||||
if tree == nil {
|
||||
return string(content)
|
||||
}
|
||||
root := tree.RootNode()
|
||||
var sb strings.Builder
|
||||
skeletonGoNode(root, content, &sb)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func skeletonGoNode(node *sitter.Node, content []byte, sb *strings.Builder) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
nodeType := node.Type()
|
||||
|
||||
if bodyField, ok := skeletonGoBodyNodes[nodeType]; ok {
|
||||
body := node.ChildByFieldName(bodyField)
|
||||
if body != nil {
|
||||
sig := strings.TrimRight(string(content[node.StartByte():body.StartByte()]), " \t")
|
||||
sb.WriteString(sig)
|
||||
sb.WriteString("{ ... }\n\n")
|
||||
return
|
||||
}
|
||||
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
||||
sb.WriteString("\n")
|
||||
return
|
||||
}
|
||||
|
||||
switch nodeType {
|
||||
case "source_file":
|
||||
for i := 0; i < int(node.ChildCount()); i++ {
|
||||
child := node.Child(i)
|
||||
if child == nil {
|
||||
continue
|
||||
}
|
||||
skeletonGoNode(child, content, sb)
|
||||
}
|
||||
return
|
||||
case "comment":
|
||||
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
||||
sb.WriteString("\n")
|
||||
return
|
||||
default:
|
||||
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- TypeScript / JavaScript skeleton ----
|
||||
|
||||
func skeletonTS(tree *sitter.Tree, content []byte) string {
|
||||
if tree == nil {
|
||||
return string(content)
|
||||
}
|
||||
root := tree.RootNode()
|
||||
var sb strings.Builder
|
||||
skeletonTSNode(root, content, &sb)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func skeletonTSNode(node *sitter.Node, content []byte, sb *strings.Builder) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
nodeType := node.Type()
|
||||
|
||||
switch nodeType {
|
||||
case "program":
|
||||
for i := 0; i < int(node.ChildCount()); i++ {
|
||||
child := node.Child(i)
|
||||
if child == nil {
|
||||
continue
|
||||
}
|
||||
skeletonTSNode(child, content, sb)
|
||||
}
|
||||
return
|
||||
case "function_declaration":
|
||||
body := node.ChildByFieldName("body")
|
||||
if body != nil {
|
||||
sig := strings.TrimRight(string(content[node.StartByte():body.StartByte()]), " \t")
|
||||
sb.WriteString(sig)
|
||||
sb.WriteString("{ ... }\n\n")
|
||||
return
|
||||
}
|
||||
case "class_declaration":
|
||||
body := node.ChildByFieldName("body")
|
||||
if body != nil {
|
||||
header := strings.TrimRight(string(content[node.StartByte():body.StartByte()]), " \t")
|
||||
sb.WriteString(header)
|
||||
sb.WriteString("{\n")
|
||||
for i := 0; i < int(body.ChildCount()); i++ {
|
||||
child := body.Child(i)
|
||||
if child == nil {
|
||||
continue
|
||||
}
|
||||
if child.Type() == "method_definition" {
|
||||
methBody := child.ChildByFieldName("body")
|
||||
if methBody != nil {
|
||||
methSig := strings.TrimRight(string(content[child.StartByte():methBody.StartByte()]), " \t")
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(methSig)
|
||||
sb.WriteString("{ ... }\n")
|
||||
continue
|
||||
}
|
||||
}
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(string(content[child.StartByte():child.EndByte()]))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString("}\n\n")
|
||||
return
|
||||
}
|
||||
case "comment":
|
||||
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
||||
sb.WriteString("\n")
|
||||
return
|
||||
}
|
||||
|
||||
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// ---- Python skeleton ----
|
||||
|
||||
func skeletonPython(tree *sitter.Tree, content []byte) string {
|
||||
if tree == nil {
|
||||
return string(content)
|
||||
}
|
||||
root := tree.RootNode()
|
||||
var sb strings.Builder
|
||||
skeletonPythonNode(root, content, &sb, "")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func skeletonPythonNode(node *sitter.Node, content []byte, sb *strings.Builder, indent string) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
nodeType := node.Type()
|
||||
|
||||
switch nodeType {
|
||||
case "module":
|
||||
for i := 0; i < int(node.ChildCount()); i++ {
|
||||
child := node.Child(i)
|
||||
if child == nil {
|
||||
continue
|
||||
}
|
||||
skeletonPythonNode(child, content, sb, indent)
|
||||
}
|
||||
return
|
||||
case "function_definition", "decorated_definition":
|
||||
nodeText := string(content[node.StartByte():node.EndByte()])
|
||||
lines := strings.SplitN(nodeText, "\n", 2)
|
||||
sb.WriteString(indent)
|
||||
sb.WriteString(lines[0])
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(indent)
|
||||
sb.WriteString(" ...\n\n")
|
||||
return
|
||||
case "class_definition":
|
||||
body := node.ChildByFieldName("body")
|
||||
if body != nil {
|
||||
header := string(content[node.StartByte():body.StartByte()])
|
||||
firstLine := strings.SplitN(header, "\n", 2)[0]
|
||||
sb.WriteString(indent)
|
||||
sb.WriteString(firstLine)
|
||||
sb.WriteString("\n")
|
||||
for i := 0; i < int(body.ChildCount()); i++ {
|
||||
child := body.Child(i)
|
||||
if child == nil {
|
||||
continue
|
||||
}
|
||||
if child.Type() == "function_definition" || child.Type() == "decorated_definition" {
|
||||
childText := string(content[child.StartByte():child.EndByte()])
|
||||
childLines := strings.SplitN(childText, "\n", 2)
|
||||
sb.WriteString(indent + " ")
|
||||
sb.WriteString(childLines[0])
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(indent + " ...")
|
||||
sb.WriteString("\n")
|
||||
continue
|
||||
}
|
||||
if child.Type() == "expression_statement" {
|
||||
sb.WriteString(indent + " ")
|
||||
sb.WriteString(string(content[child.StartByte():child.EndByte()]))
|
||||
sb.WriteString("\n")
|
||||
continue
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
return
|
||||
}
|
||||
case "comment":
|
||||
sb.WriteString(indent)
|
||||
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
||||
sb.WriteString("\n")
|
||||
return
|
||||
}
|
||||
|
||||
sb.WriteString(indent)
|
||||
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// ---- Rust skeleton ----
|
||||
|
||||
func skeletonRust(tree *sitter.Tree, content []byte) string {
|
||||
if tree == nil {
|
||||
return string(content)
|
||||
}
|
||||
root := tree.RootNode()
|
||||
var sb strings.Builder
|
||||
skeletonRustNode(root, content, &sb)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func skeletonRustNode(node *sitter.Node, content []byte, sb *strings.Builder) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
nodeType := node.Type()
|
||||
|
||||
switch nodeType {
|
||||
case "source_file":
|
||||
for i := 0; i < int(node.ChildCount()); i++ {
|
||||
child := node.Child(i)
|
||||
if child == nil {
|
||||
continue
|
||||
}
|
||||
skeletonRustNode(child, content, sb)
|
||||
}
|
||||
return
|
||||
case "function_item":
|
||||
body := node.ChildByFieldName("body")
|
||||
if body != nil {
|
||||
sig := strings.TrimRight(string(content[node.StartByte():body.StartByte()]), " \t")
|
||||
sb.WriteString(sig)
|
||||
sb.WriteString("{ ... }\n\n")
|
||||
return
|
||||
}
|
||||
case "impl_item":
|
||||
body := node.ChildByFieldName("body")
|
||||
if body != nil {
|
||||
header := strings.TrimRight(string(content[node.StartByte():body.StartByte()]), " \t")
|
||||
sb.WriteString(header)
|
||||
sb.WriteString("{\n")
|
||||
for i := 0; i < int(body.ChildCount()); i++ {
|
||||
child := body.Child(i)
|
||||
if child == nil {
|
||||
continue
|
||||
}
|
||||
if child.Type() == "function_item" {
|
||||
methBody := child.ChildByFieldName("body")
|
||||
if methBody != nil {
|
||||
methSig := strings.TrimRight(string(content[child.StartByte():methBody.StartByte()]), " \t")
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(methSig)
|
||||
sb.WriteString("{ ... }\n")
|
||||
continue
|
||||
}
|
||||
}
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(string(content[child.StartByte():child.EndByte()]))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString("}\n\n")
|
||||
return
|
||||
}
|
||||
case "line_comment", "block_comment":
|
||||
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
||||
sb.WriteString("\n")
|
||||
return
|
||||
}
|
||||
|
||||
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/lukaszraczylo/mcp-filepuff/pkg/protocol"
|
||||
)
|
||||
|
||||
// StripFlag names the categories of content to remove.
|
||||
type StripFlag string
|
||||
|
||||
const (
|
||||
StripImports StripFlag = "imports"
|
||||
StripLicense StripFlag = "license"
|
||||
StripBlockComments StripFlag = "block_comments"
|
||||
)
|
||||
|
||||
// StripResult holds the stripped content and which flags actually removed content.
|
||||
type StripResult struct {
|
||||
Content string
|
||||
Stripped []StripFlag
|
||||
}
|
||||
|
||||
// StripContent applies requested strip operations to content, in order:
|
||||
// license → imports → block_comments.
|
||||
// lang is used to pick language-specific heuristics.
|
||||
func StripContent(content string, flags []StripFlag, lang protocol.Language) StripResult {
|
||||
flagSet := make(map[StripFlag]bool, len(flags))
|
||||
for _, f := range flags {
|
||||
flagSet[f] = true
|
||||
}
|
||||
|
||||
var stripped []StripFlag
|
||||
|
||||
if flagSet[StripLicense] {
|
||||
next, removed := stripLicense(content)
|
||||
if removed {
|
||||
content = next
|
||||
stripped = append(stripped, StripLicense)
|
||||
}
|
||||
}
|
||||
|
||||
if flagSet[StripImports] {
|
||||
next, removed := stripImports(content, lang)
|
||||
if removed {
|
||||
content = next
|
||||
stripped = append(stripped, StripImports)
|
||||
}
|
||||
}
|
||||
|
||||
if flagSet[StripBlockComments] {
|
||||
next, removed := stripBlockComments(content, lang)
|
||||
if removed {
|
||||
content = next
|
||||
stripped = append(stripped, StripBlockComments)
|
||||
}
|
||||
}
|
||||
|
||||
return StripResult{Content: content, Stripped: stripped}
|
||||
}
|
||||
|
||||
// stripLicense removes a leading block comment that looks like a license header.
|
||||
// A comment qualifies if it contains "copyright", "license", or "spdx-license-identifier" (case-insensitive).
|
||||
func stripLicense(content string) (string, bool) {
|
||||
trimmed := strings.TrimLeft(content, " \t\n\r")
|
||||
|
||||
// C-style block comment at top
|
||||
if strings.HasPrefix(trimmed, "/*") {
|
||||
end := strings.Index(trimmed, "*/")
|
||||
if end >= 0 {
|
||||
candidate := trimmed[:end+2]
|
||||
lower := strings.ToLower(candidate)
|
||||
if strings.Contains(lower, "copyright") ||
|
||||
strings.Contains(lower, "license") ||
|
||||
strings.Contains(lower, "spdx-license-identifier") {
|
||||
rest := trimmed[end+2:]
|
||||
// Consume trailing newline(s)
|
||||
rest = strings.TrimLeft(rest, "\r\n")
|
||||
return rest, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Python/hash-style leading comment block
|
||||
if strings.HasPrefix(trimmed, "#") {
|
||||
lines := strings.Split(trimmed, "\n")
|
||||
var commentLines []string
|
||||
var rest []string
|
||||
inComment := true
|
||||
for i, l := range lines {
|
||||
if inComment && (strings.HasPrefix(l, "#") || strings.TrimSpace(l) == "") {
|
||||
commentLines = append(commentLines, l)
|
||||
} else {
|
||||
rest = lines[i:]
|
||||
break
|
||||
}
|
||||
}
|
||||
block := strings.Join(commentLines, "\n")
|
||||
lower := strings.ToLower(block)
|
||||
if strings.Contains(lower, "copyright") ||
|
||||
strings.Contains(lower, "license") ||
|
||||
strings.Contains(lower, "spdx-license-identifier") {
|
||||
return strings.Join(rest, "\n"), true
|
||||
}
|
||||
}
|
||||
|
||||
return content, false
|
||||
}
|
||||
|
||||
// stripImports removes top-of-file import blocks, language-specific.
|
||||
func stripImports(content string, lang protocol.Language) (string, bool) {
|
||||
switch lang {
|
||||
case protocol.LangGo:
|
||||
return stripGoImports(content)
|
||||
case protocol.LangTypeScript, protocol.LangJavaScript:
|
||||
return stripTSImports(content)
|
||||
case protocol.LangPython:
|
||||
return stripPythonImports(content)
|
||||
case protocol.LangRust:
|
||||
return stripRustImports(content)
|
||||
default:
|
||||
return content, false
|
||||
}
|
||||
}
|
||||
|
||||
// stripGoImports removes Go import(...) or single import "..." declarations.
|
||||
func stripGoImports(content string) (string, bool) {
|
||||
lines := strings.Split(content, "\n")
|
||||
var out []string
|
||||
removed := false
|
||||
i := 0
|
||||
for i < len(lines) {
|
||||
trimLine := strings.TrimSpace(lines[i])
|
||||
if strings.HasPrefix(trimLine, "import (") || trimLine == "import (" {
|
||||
// multi-line import block
|
||||
removed = true
|
||||
i++ // skip "import ("
|
||||
for i < len(lines) {
|
||||
if strings.TrimSpace(lines[i]) == ")" {
|
||||
i++ // skip closing ")"
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
// skip one blank line after
|
||||
if i < len(lines) && strings.TrimSpace(lines[i]) == "" {
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimLine, `import "`) || strings.HasPrefix(trimLine, "import `") {
|
||||
removed = true
|
||||
i++
|
||||
continue
|
||||
}
|
||||
out = append(out, lines[i])
|
||||
i++
|
||||
}
|
||||
if !removed {
|
||||
return content, false
|
||||
}
|
||||
return strings.Join(out, "\n"), true
|
||||
}
|
||||
|
||||
// stripTSImports removes TypeScript/JavaScript "import ... from ..." and "require(...)" lines.
|
||||
func stripTSImports(content string) (string, bool) {
|
||||
lines := strings.Split(content, "\n")
|
||||
var out []string
|
||||
removed := false
|
||||
for _, l := range lines {
|
||||
trimLine := strings.TrimSpace(l)
|
||||
if strings.HasPrefix(trimLine, "import ") || strings.HasPrefix(trimLine, "const {") && strings.Contains(trimLine, "require(") {
|
||||
removed = true
|
||||
continue
|
||||
}
|
||||
out = append(out, l)
|
||||
}
|
||||
if !removed {
|
||||
return content, false
|
||||
}
|
||||
return strings.Join(out, "\n"), true
|
||||
}
|
||||
|
||||
// stripPythonImports removes Python "import ..." and "from ... import ..." lines.
|
||||
func stripPythonImports(content string) (string, bool) {
|
||||
lines := strings.Split(content, "\n")
|
||||
var out []string
|
||||
removed := false
|
||||
for _, l := range lines {
|
||||
trimLine := strings.TrimSpace(l)
|
||||
if strings.HasPrefix(trimLine, "import ") || strings.HasPrefix(trimLine, "from ") {
|
||||
removed = true
|
||||
continue
|
||||
}
|
||||
out = append(out, l)
|
||||
}
|
||||
if !removed {
|
||||
return content, false
|
||||
}
|
||||
return strings.Join(out, "\n"), true
|
||||
}
|
||||
|
||||
// stripRustImports removes Rust "use ..." declarations.
|
||||
func stripRustImports(content string) (string, bool) {
|
||||
lines := strings.Split(content, "\n")
|
||||
var out []string
|
||||
removed := false
|
||||
inMulti := false
|
||||
for _, l := range lines {
|
||||
trimLine := strings.TrimSpace(l)
|
||||
if inMulti {
|
||||
// look for semicolon terminating multi-line use
|
||||
if strings.Contains(trimLine, ";") {
|
||||
inMulti = false
|
||||
}
|
||||
removed = true
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimLine, "use ") {
|
||||
removed = true
|
||||
if !strings.HasSuffix(trimLine, ";") {
|
||||
inMulti = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
out = append(out, l)
|
||||
}
|
||||
if !removed {
|
||||
return content, false
|
||||
}
|
||||
return strings.Join(out, "\n"), true
|
||||
}
|
||||
|
||||
// stripBlockComments removes /* ... */ block comments (Go/TS/C/Rust)
|
||||
// and Python triple-quoted docstrings.
|
||||
func stripBlockComments(content string, lang protocol.Language) (string, bool) {
|
||||
if lang == protocol.LangPython {
|
||||
return stripPythonDocstrings(content)
|
||||
}
|
||||
return stripCStyleBlockComments(content)
|
||||
}
|
||||
|
||||
// stripCStyleBlockComments removes /* ... */ from content.
|
||||
func stripCStyleBlockComments(content string) (string, bool) {
|
||||
removed := false
|
||||
var sb strings.Builder
|
||||
i := 0
|
||||
for i < len(content) {
|
||||
if i+1 < len(content) && content[i] == '/' && content[i+1] == '*' {
|
||||
// find closing */
|
||||
end := strings.Index(content[i+2:], "*/")
|
||||
if end >= 0 {
|
||||
removed = true
|
||||
// advance past */
|
||||
i = i + 2 + end + 2
|
||||
// consume trailing newline
|
||||
if i < len(content) && content[i] == '\n' {
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
sb.WriteByte(content[i])
|
||||
i++
|
||||
}
|
||||
if !removed {
|
||||
return content, false
|
||||
}
|
||||
return sb.String(), true
|
||||
}
|
||||
|
||||
// stripPythonDocstrings removes triple-quoted strings (""" and ”').
|
||||
func stripPythonDocstrings(content string) (string, bool) {
|
||||
removed := false
|
||||
var sb strings.Builder
|
||||
i := 0
|
||||
for i < len(content) {
|
||||
if i+2 < len(content) {
|
||||
triple := content[i : i+3]
|
||||
if triple == `"""` || triple == `'''` {
|
||||
end := strings.Index(content[i+3:], triple)
|
||||
if end >= 0 {
|
||||
removed = true
|
||||
i = i + 3 + end + 3
|
||||
if i < len(content) && content[i] == '\n' {
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.WriteByte(content[i])
|
||||
i++
|
||||
}
|
||||
if !removed {
|
||||
return content, false
|
||||
}
|
||||
return sb.String(), true
|
||||
}
|
||||
Reference in New Issue
Block a user