use markdown renderer in preview when previewing multi-file skills
This commit is contained in:
parent
8ea84d0dee
commit
ba61ded4b3
2 changed files with 135 additions and 13 deletions
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
|
|
@ -24,6 +25,7 @@ type previewOptions struct {
|
|||
HttpClient func() (*http.Client, error)
|
||||
Prompter prompter.Prompter
|
||||
Executable func() string
|
||||
RenderFile func(string, string) string
|
||||
|
||||
RepoArg string
|
||||
SkillName string
|
||||
|
|
@ -39,6 +41,9 @@ func NewCmdPreview(f *cmdutil.Factory, runF func(*previewOptions) error) *cobra.
|
|||
Prompter: f.Prompter,
|
||||
Executable: f.Executable,
|
||||
}
|
||||
opts.RenderFile = func(filePath, content string) string {
|
||||
return renderMarkdownPreview(opts.IO, filePath, content)
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "preview <repository> [<skill>]",
|
||||
|
|
@ -139,18 +144,7 @@ func previewRun(opts *previewOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
parsed, parseErr := frontmatter.Parse(content)
|
||||
if parseErr == nil {
|
||||
content = parsed.Body
|
||||
}
|
||||
|
||||
rendered, err := markdown.Render(content,
|
||||
markdown.WithTheme(opts.IO.TerminalTheme()),
|
||||
markdown.WithWrap(opts.IO.TerminalWidth()),
|
||||
markdown.WithoutIndentation())
|
||||
if err != nil {
|
||||
rendered = content
|
||||
}
|
||||
rendered := opts.renderFile("SKILL.md", content)
|
||||
|
||||
// Collect extra files (everything that isn't SKILL.md)
|
||||
var extraFiles []discovery.SkillFile
|
||||
|
|
@ -268,7 +262,7 @@ func renderInteractive(opts *previewOptions, cs *iostreams.ColorScheme, skill di
|
|||
fmt.Fprintf(opts.IO.ErrOut, "%s could not fetch %s: %v\n", cs.Red("!"), selectedFile.Path, fetchErr)
|
||||
continue
|
||||
}
|
||||
content = fileContent
|
||||
content = renderSelectedFilePreview(opts, selectedFile.Path, fileContent)
|
||||
if !strings.HasSuffix(content, "\n") {
|
||||
content += "\n"
|
||||
}
|
||||
|
|
@ -282,6 +276,50 @@ func renderInteractive(opts *previewOptions, cs *iostreams.ColorScheme, skill di
|
|||
}
|
||||
}
|
||||
|
||||
func (opts *previewOptions) renderFile(filePath, content string) string {
|
||||
if opts.RenderFile != nil {
|
||||
return opts.RenderFile(filePath, content)
|
||||
}
|
||||
|
||||
return renderMarkdownPreview(opts.IO, filePath, content)
|
||||
}
|
||||
|
||||
func renderSelectedFilePreview(opts *previewOptions, filePath, content string) string {
|
||||
if !isMarkdownFile(filePath) {
|
||||
return content
|
||||
}
|
||||
|
||||
return opts.renderFile(filePath, content)
|
||||
}
|
||||
|
||||
func renderMarkdownPreview(io *iostreams.IOStreams, filePath, content string) string {
|
||||
if filePath == "SKILL.md" {
|
||||
parsed, err := frontmatter.Parse(content)
|
||||
if err == nil {
|
||||
content = parsed.Body
|
||||
}
|
||||
}
|
||||
|
||||
rendered, err := markdown.Render(content,
|
||||
markdown.WithTheme(io.TerminalTheme()),
|
||||
markdown.WithWrap(io.TerminalWidth()),
|
||||
markdown.WithoutIndentation())
|
||||
if err != nil {
|
||||
return content
|
||||
}
|
||||
|
||||
return rendered
|
||||
}
|
||||
|
||||
func isMarkdownFile(filePath string) bool {
|
||||
switch strings.ToLower(path.Ext(filePath)) {
|
||||
case ".md", ".markdown", ".mdown", ".mkd", ".mkdn":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func selectSkill(opts *previewOptions, skills []discovery.Skill) (discovery.Skill, error) {
|
||||
if opts.SkillName != "" {
|
||||
for _, s := range skills {
|
||||
|
|
|
|||
|
|
@ -445,6 +445,90 @@ func TestPreviewRun_ShowsFileTree(t *testing.T) {
|
|||
assert.Equal(t, 2, selectCalls)
|
||||
})
|
||||
|
||||
t.Run("interactive markdown file uses markdown renderer", func(t *testing.T) {
|
||||
readmeContent := "# Usage\n\nUse **carefully**."
|
||||
encodedReadme := base64.StdEncoding.EncodeToString([]byte(readmeContent))
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/owner/repo/releases/latest"),
|
||||
httpmock.StringResponse(`{"tag_name": "v1.0.0"}`),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/owner/repo/git/ref/tags/v1.0.0"),
|
||||
httpmock.StringResponse(`{"object": {"sha": "abc123", "type": "commit"}}`),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/owner/repo/git/trees/abc123"),
|
||||
httpmock.StringResponse(`{
|
||||
"sha": "abc123",
|
||||
"truncated": false,
|
||||
"tree": [
|
||||
{"path": "skills/my-skill", "type": "tree", "sha": "treeSHA"},
|
||||
{"path": "skills/my-skill/SKILL.md", "type": "blob", "sha": "blobSKILL"},
|
||||
{"path": "skills/my-skill/README.md", "type": "blob", "sha": "blobREADME"}
|
||||
]
|
||||
}`),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/owner/repo/git/trees/treeSHA"),
|
||||
httpmock.StringResponse(`{
|
||||
"tree": [
|
||||
{"path": "SKILL.md", "type": "blob", "sha": "blobSKILL", "size": 50},
|
||||
{"path": "README.md", "type": "blob", "sha": "blobREADME", "size": 28}
|
||||
]
|
||||
}`),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/owner/repo/git/blobs/blobSKILL"),
|
||||
httpmock.StringResponse(`{"sha": "blobSKILL", "content": "`+encodedContent+`", "encoding": "base64"}`),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/owner/repo/git/blobs/blobREADME"),
|
||||
httpmock.StringResponse(`{"sha": "blobREADME", "content": "`+encodedReadme+`", "encoding": "base64"}`),
|
||||
)
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
ios.SetStdinTTY(true)
|
||||
ios.SetColorEnabled(false)
|
||||
|
||||
renderCalls := 0
|
||||
|
||||
selectCalls := 0
|
||||
pm := &prompter.PrompterMock{
|
||||
SelectFunc: func(prompt string, defaultValue string, options []string) (int, error) {
|
||||
selectCalls++
|
||||
if selectCalls == 1 {
|
||||
assert.Equal(t, []string{"SKILL.md", "README.md"}, options)
|
||||
return 1, nil
|
||||
}
|
||||
return 0, fmt.Errorf("user cancelled")
|
||||
},
|
||||
}
|
||||
|
||||
opts := &previewOptions{
|
||||
IO: ios,
|
||||
HttpClient: func() (*http.Client, error) { return &http.Client{Transport: reg}, nil },
|
||||
Prompter: pm,
|
||||
repo: ghrepo.New("owner", "repo"),
|
||||
SkillName: "my-skill",
|
||||
RenderFile: func(filePath, content string) string {
|
||||
renderCalls++
|
||||
return fmt.Sprintf("rendered:%s", filePath)
|
||||
},
|
||||
}
|
||||
|
||||
err := previewRun(opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := stdout.String()
|
||||
assert.Contains(t, out, "rendered:README.md")
|
||||
assert.Equal(t, 2, selectCalls)
|
||||
assert.Equal(t, 2, renderCalls)
|
||||
})
|
||||
|
||||
t.Run("non-interactive dumps all files", func(t *testing.T) {
|
||||
reg := makeReg()
|
||||
defer reg.Verify(t)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue