5c807d76a0
Extracted from Cyrene main repo (backend/pkg/plugins + backend/plugin-manager). Contains SDK interfaces (Plugin/Tool/HostAPI), 13 built-in plugins, ToolRegistry with call log ring buffer, and Plugin Manager REST API service. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
185 lines
6.6 KiB
Go
185 lines
6.6 KiB
Go
package markdown
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene-Plugins/sdk"
|
|
)
|
|
|
|
type MarkdownPlugin struct{ sdk.BasePlugin }
|
|
|
|
func (p *MarkdownPlugin) Metadata() sdk.PluginMetadata {
|
|
return sdk.PluginMetadata{
|
|
Name: "markdown", DisplayName: "Markdown Processor", Version: "1.0.0",
|
|
Description: "Markdown processing: to HTML, extract text/links/code, generate TOC",
|
|
Category: "format", Author: sdk.PluginAuthor{Name: "Cyrene Team"},
|
|
}
|
|
}
|
|
|
|
func (p *MarkdownPlugin) Tools() []sdk.Tool { return []sdk.Tool{&MarkdownTool{}} }
|
|
|
|
type MarkdownTool struct{ sdk.BaseTool }
|
|
|
|
func (t *MarkdownTool) Definition() sdk.ToolDefinition {
|
|
return sdk.ToolDefinition{
|
|
ID: "markdown", Name: "markdown", DisplayName: "Markdown Processor",
|
|
Description: "Markdown processing. Convert to HTML, extract plain text, extract links/code blocks, generate TOC.",
|
|
Category: "format", Complexity: sdk.ComplexitySimple,
|
|
Parameters: map[string]interface{}{
|
|
"type": "object",
|
|
"properties": map[string]interface{}{
|
|
"action": map[string]interface{}{"type": "string", "enum": []string{"to_html", "to_text", "extract_links", "extract_code", "table_of_contents"}},
|
|
"markdown": map[string]interface{}{"type": "string"},
|
|
},
|
|
"required": []string{"action", "markdown"},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (t *MarkdownTool) Validate(args map[string]interface{}) error {
|
|
for _, k := range []string{"action", "markdown"} {
|
|
if _, ok := args[k]; !ok {
|
|
return fmt.Errorf("missing required parameter: %s", k)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (t *MarkdownTool) Execute(_ context.Context, args map[string]interface{}) (*sdk.ToolResult, error) {
|
|
action, _ := args["action"].(string)
|
|
md, _ := args["markdown"].(string)
|
|
|
|
switch action {
|
|
case "to_html":
|
|
return &sdk.ToolResult{ToolName: "markdown", Success: true, Output: mdToHTML(md)}, nil
|
|
|
|
case "to_text":
|
|
text := md
|
|
reCode := regexp.MustCompile("(?s)```.*?```")
|
|
text = reCode.ReplaceAllString(text, "")
|
|
text = regexp.MustCompile(`\*\*([^*]+)\*\*`).ReplaceAllString(text, "$1")
|
|
text = regexp.MustCompile(`\*([^*]+)\*`).ReplaceAllString(text, "$1")
|
|
text = regexp.MustCompile(`~~([^~]+)~~`).ReplaceAllString(text, "$1")
|
|
text = regexp.MustCompile(`^#{1,6}\s+`).ReplaceAllString(text, "")
|
|
text = regexp.MustCompile(`^[*-]\s+`).ReplaceAllString(text, "- ")
|
|
text = regexp.MustCompile(`^>\s+`).ReplaceAllString(text, "")
|
|
text = regexp.MustCompile(`\n{3,}`).ReplaceAllString(text, "\n\n")
|
|
return &sdk.ToolResult{ToolName: "markdown", Success: true, Output: strings.TrimSpace(text)}, nil
|
|
|
|
case "extract_links":
|
|
re := regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`)
|
|
matches := re.FindAllStringSubmatch(md, -1)
|
|
if len(matches) == 0 {
|
|
return &sdk.ToolResult{ToolName: "markdown", Success: true, Output: "No links found"}, nil
|
|
}
|
|
var out strings.Builder
|
|
for i, m := range matches {
|
|
out.WriteString(fmt.Sprintf("%d. %s -> %s\n", i+1, m[1], m[2]))
|
|
}
|
|
return &sdk.ToolResult{ToolName: "markdown", Success: true, Output: out.String()}, nil
|
|
|
|
case "extract_code":
|
|
re := regexp.MustCompile("(?s)```(\\w*)\n?(.*?)```")
|
|
matches := re.FindAllStringSubmatch(md, -1)
|
|
if len(matches) == 0 {
|
|
return &sdk.ToolResult{ToolName: "markdown", Success: true, Output: "No code blocks found"}, nil
|
|
}
|
|
var out strings.Builder
|
|
for i, m := range matches {
|
|
lang := m[1]
|
|
if lang == "" {
|
|
lang = "text"
|
|
}
|
|
code := m[2]
|
|
if len([]rune(code)) > 500 {
|
|
code = string([]rune(code)[:500]) + "..."
|
|
}
|
|
out.WriteString(fmt.Sprintf("--- Block %d (%s) ---\n%s\n\n", i+1, lang, code))
|
|
}
|
|
return &sdk.ToolResult{ToolName: "markdown", Success: true, Output: out.String()}, nil
|
|
|
|
case "table_of_contents":
|
|
re := regexp.MustCompile(`(?m)^(#{1,6})\s+(.+)$`)
|
|
matches := re.FindAllStringSubmatch(md, -1)
|
|
if len(matches) == 0 {
|
|
return &sdk.ToolResult{ToolName: "markdown", Success: true, Output: "No headings found"}, nil
|
|
}
|
|
var out strings.Builder
|
|
for _, m := range matches {
|
|
depth := len(m[1])
|
|
indent := strings.Repeat(" ", depth-1)
|
|
out.WriteString(fmt.Sprintf("%s- %s\n", indent, m[2]))
|
|
}
|
|
return &sdk.ToolResult{ToolName: "markdown", Success: true, Output: out.String()}, nil
|
|
}
|
|
return &sdk.ToolResult{ToolName: "markdown", Success: false, Error: "unknown action: " + action}, nil
|
|
}
|
|
|
|
func mdToHTML(md string) string {
|
|
// Save code blocks
|
|
type placeholder struct {
|
|
orig string
|
|
content string
|
|
language string
|
|
}
|
|
blocks := []*placeholder{}
|
|
reCode := regexp.MustCompile("(?s)```(\\w*)\n?(.*?)```")
|
|
md = reCode.ReplaceAllStringFunc(md, func(s string) string {
|
|
m := reCode.FindStringSubmatch(s)
|
|
b := &placeholder{orig: fmt.Sprintf("\x00CODE%d\x00", len(blocks)), language: m[1], content: escapeHTML(m[2])}
|
|
blocks = append(blocks, b)
|
|
return b.orig
|
|
})
|
|
|
|
// Inline elements
|
|
md = regexp.MustCompile("`([^`]+)`").ReplaceAllString(md, "<code>$1</code>")
|
|
md = regexp.MustCompile(`!\[([^\]]*)\]\(([^)]+)\)`).ReplaceAllString(md, `<img src="$2" alt="$1">`)
|
|
md = regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`).ReplaceAllString(md, `<a href="$2">$1</a>`)
|
|
md = regexp.MustCompile(`\*\*([^*]+)\*\*`).ReplaceAllString(md, `<strong>$1</strong>`)
|
|
md = regexp.MustCompile(`\*([^*]+)\*`).ReplaceAllString(md, `<em>$1</em>`)
|
|
md = regexp.MustCompile(`~~([^~]+)~~`).ReplaceAllString(md, `<del>$1</del>`)
|
|
md = regexp.MustCompile(`(?m)^#{6}\s+(.+)$`).ReplaceAllString(md, `<h6>$1</h6>`)
|
|
md = regexp.MustCompile(`(?m)^#{5}\s+(.+)$`).ReplaceAllString(md, `<h5>$1</h5>`)
|
|
md = regexp.MustCompile(`(?m)^#{4}\s+(.+)$`).ReplaceAllString(md, `<h4>$1</h4>`)
|
|
md = regexp.MustCompile(`(?m)^#{3}\s+(.+)$`).ReplaceAllString(md, `<h3>$1</h3>`)
|
|
md = regexp.MustCompile(`(?m)^#{2}\s+(.+)$`).ReplaceAllString(md, `<h2>$1</h2>`)
|
|
md = regexp.MustCompile(`(?m)^#{1}\s+(.+)$`).ReplaceAllString(md, `<h1>$1</h1>`)
|
|
md = regexp.MustCompile(`(?m)^---\s*$`).ReplaceAllString(md, `<hr>`)
|
|
md = regexp.MustCompile(`(?m)^>\s+(.+)$`).ReplaceAllString(md, `<blockquote>$1</blockquote>`)
|
|
|
|
// Restore code blocks
|
|
for _, b := range blocks {
|
|
langAttr := ""
|
|
if b.language != "" {
|
|
langAttr = " class=\"language-" + b.language + "\""
|
|
}
|
|
md = strings.Replace(md, b.orig, "<pre><code"+langAttr+">"+b.content+"</code></pre>", 1)
|
|
}
|
|
|
|
// Paragraphs
|
|
lines := strings.Split(md, "\n")
|
|
var out strings.Builder
|
|
for _, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(trimmed, "<") {
|
|
out.WriteString(trimmed + "\n")
|
|
} else {
|
|
out.WriteString("<p>" + trimmed + "</p>\n")
|
|
}
|
|
}
|
|
return out.String()
|
|
}
|
|
|
|
func escapeHTML(s string) string {
|
|
s = strings.ReplaceAll(s, "&", "&")
|
|
s = strings.ReplaceAll(s, "<", "<")
|
|
s = strings.ReplaceAll(s, ">", ">")
|
|
return s
|
|
}
|