Files
Cyrene_v2/plugins/tools/basic/markdown.go
T

194 lines
5.4 KiB
Go

package basic
import (
"context"
"fmt"
"strings"
)
// markdown exposes Markdown building and parsing helpers: tables, lists, headings
// and YAML front-matter metadata.
type markdown struct{}
const (
// minHeadingLevel and maxHeadingLevel bound the supported ATX heading depth.
minHeadingLevel = 1
maxHeadingLevel = 6
)
// Name returns the tool identifier.
func (markdown) Name() string { return "markdown" }
// Description explains what the tool does for the model.
func (markdown) Description() string {
return "Build or parse Markdown: tables, lists, headings and YAML front-matter metadata."
}
// Parameters returns the tool input JSON Schema. Operation-specific inputs are
// validated by Execute.
func (markdown) Parameters() (map[string]any, error) {
return map[string]any{
"type": "object",
"properties": map[string]any{
"operation": map[string]any{
"type": "string",
"enum": []any{"table", "list", "heading", "metadata"},
"description": "the operation to perform",
},
"content": map[string]any{"type": "string", "description": "Markdown body for metadata parsing"},
"headers": map[string]any{"type": "array", "description": "table column headers"},
"rows": map[string]any{"type": "array", "description": "table rows (array of arrays of strings)"},
"items": map[string]any{"type": "array", "description": "list entries"},
"ordered": map[string]any{"type": "boolean", "description": "render a numbered list when true"},
"level": map[string]any{"type": "integer", "description": "heading level 1..6"},
"text": map[string]any{"type": "string", "description": "heading text"},
},
"required": []any{"operation"},
}, nil
}
// Execute runs the requested operation.
func (markdown) Execute(ctx context.Context, args any) (any, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
m, err := asMap(args)
if err != nil {
return nil, err
}
op, err := reqString(m, "operation")
if err != nil {
return nil, err
}
switch strings.ToLower(strings.TrimSpace(op)) {
case "table":
return mdTable(m)
case "list":
return mdList(m)
case "heading":
return mdHeading(m)
case "metadata":
return mdMeta(m)
default:
return nil, fmt.Errorf("basic: unknown markdown operation %q: %w", op, ErrUnsupported)
}
}
// mdTable builds a Markdown table from headers and rows.
func mdTable(m map[string]any) (any, error) {
headers, err := strSliceArg(m, "headers")
if err != nil {
return nil, err
}
rows, err := strSliceSliceArg(m, "rows")
if err != nil {
return nil, err
}
var b strings.Builder
write := func(cells []string) {
b.WriteString("| ")
for i, c := range cells {
if i > 0 {
b.WriteString(" | ")
}
b.WriteString(escapeCell(c))
}
b.WriteString(" |\n")
}
write(headers)
b.WriteString(tableSeparator(headers))
for _, r := range rows {
write(r)
}
return map[string]any{"result": b.String()}, nil
}
// tableSeparator returns the | --- | --- | header separator row for the column count.
func tableSeparator(cols []string) string {
parts := make([]string, len(cols))
for i := range parts {
parts[i] = "---"
}
return "| " + strings.Join(parts, " | ") + " |\n"
}
// escapeCell neutralizes a cell so a pipe inside the value does not break the table.
func escapeCell(s string) string {
s = strings.ReplaceAll(s, "|", `\|`)
s = strings.ReplaceAll(s, "\n", " ")
return strings.ReplaceAll(s, "\r", "")
}
// mdList builds a bullet or numbered list from items.
func mdList(m map[string]any) (any, error) {
items, err := strSliceArg(m, "items")
if err != nil {
return nil, err
}
ordered, err := optBool(m, "ordered", false)
if err != nil {
return nil, err
}
var b strings.Builder
for i, it := range items {
if ordered {
fmt.Fprintf(&b, "%d. %s\n", i+1, it)
} else {
b.WriteString("- " + it + "\n")
}
}
return map[string]any{"result": b.String()}, nil
}
// mdHeading builds a Markdown ATX heading.
func mdHeading(m map[string]any) (any, error) {
level, err := optInt(m, "level", 1)
if err != nil {
return nil, err
}
text, err := reqString(m, "text")
if err != nil {
return nil, err
}
if level < minHeadingLevel || level > maxHeadingLevel {
return nil, fmt.Errorf("basic: heading level must be between %d and %d: %w", minHeadingLevel, maxHeadingLevel, ErrInvalidArgument)
}
return map[string]any{"result": strings.Repeat("#", level) + " " + text + "\n"}, nil
}
// mdMeta parses YAML front matter from content and returns the metadata table plus
// the remaining body.
func mdMeta(m map[string]any) (any, error) {
content, err := reqString(m, "content")
if err != nil {
return nil, err
}
meta, body := parseFrontMatter(content)
return map[string]any{"meta": meta, "body": body}, nil
}
// parseFrontMatter splits leading `--- ... ---` front matter from a Markdown body.
// When the document has no front matter it returns an empty table and the whole
// content as the body.
func parseFrontMatter(content string) (map[string]string, string) {
lines := strings.Split(content, "\n")
if len(lines) == 0 || strings.TrimSpace(lines[0]) != "---" {
return map[string]string{}, content
}
meta := map[string]string{}
i := 1
for i < len(lines) {
line := strings.TrimRight(lines[i], "\r")
if strings.TrimSpace(line) == "---" {
i++
break
}
if key, val, ok := strings.Cut(line, ":"); ok {
meta[strings.TrimSpace(key)] = strings.TrimSpace(val)
}
i++
}
body := strings.Join(lines[i:], "\n")
return meta, body
}