194 lines
5.4 KiB
Go
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
|
|
}
|