mirror of
https://github.com/charmbracelet/glow.git
synced 2026-08-20 23:24:18 +02:00
feat: detect extensionless remote JSON responses
Remote HTTP sources without file extensions were always treated as markdown, even when the server identified the payload as JSON. That made API endpoints render as prose instead of a highlighted code block. Record the HTTP Content-Type for remote sources and use JSON media types, including +json variants, as the code block language only when the URL has no extension. Fixes #910
This commit is contained in:
parent
53788271b3
commit
6cb86a1338
4 changed files with 95 additions and 9 deletions
|
|
@ -49,7 +49,7 @@ func findGitHubREADME(u *url.URL) (*source, error) {
|
|||
}
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return &source{resp.Body, result.DownloadURL}, nil
|
||||
return &source{reader: resp.Body, URL: result.DownloadURL}, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ func findGitLabREADME(u *url.URL) (*source, error) {
|
|||
}
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return &source{resp.Body, readmeRawURL}, nil
|
||||
return &source{reader: resp.Body, URL: readmeRawURL}, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
53
glow_test.go
53
glow_test.go
|
|
@ -1,6 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
|
@ -39,3 +42,53 @@ func TestGlowFlags(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceDetectsExtensionlessJSONContentType(t *testing.T) {
|
||||
src := source{
|
||||
URL: "https://api.example.com/endpoint",
|
||||
contentType: "application/vnd.api+json; charset=utf-8",
|
||||
}
|
||||
|
||||
if !src.isCode() {
|
||||
t.Fatal("expected extensionless JSON response to render as code")
|
||||
}
|
||||
if got := src.codeBlockLanguage(); got != ".json" {
|
||||
t.Fatalf("expected .json code block language, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceMarkdownExtensionTakesPrecedenceOverContentType(t *testing.T) {
|
||||
src := source{
|
||||
URL: "https://example.com/README.md",
|
||||
contentType: "application/json",
|
||||
}
|
||||
|
||||
if src.isCode() {
|
||||
t.Fatal("expected markdown extension to render as markdown")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceFromArgRecordsHTTPContentType(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
fmt.Fprint(w, `{"ok":true}`)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
src, err := sourceFromArg(server.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := src.reader.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
if got := src.contentType; got != "application/json; charset=utf-8" {
|
||||
t.Fatalf("expected content type to be recorded, got %q", got)
|
||||
}
|
||||
if !src.isCode() {
|
||||
t.Fatal("expected extensionless JSON HTTP source to render as code")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
47
main.go
47
main.go
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
|
@ -67,8 +68,9 @@ var (
|
|||
|
||||
// source provides a readable markdown source.
|
||||
type source struct {
|
||||
reader io.ReadCloser
|
||||
URL string
|
||||
reader io.ReadCloser
|
||||
URL string
|
||||
contentType string
|
||||
}
|
||||
|
||||
// sourceFromArg parses an argument and creates a readable source for it.
|
||||
|
|
@ -99,7 +101,11 @@ func sourceFromArg(arg string) (*source, error) {
|
|||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP status %d", resp.StatusCode)
|
||||
}
|
||||
return &source{resp.Body, u.String()}, nil
|
||||
return &source{
|
||||
reader: resp.Body,
|
||||
URL: u.String(),
|
||||
contentType: resp.Header.Get("Content-Type"),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -123,7 +129,7 @@ func sourceFromArg(arg string) (*source, error) {
|
|||
}
|
||||
|
||||
u, _ := filepath.Abs(path)
|
||||
src = &source{r, u}
|
||||
src = &source{reader: r, URL: u}
|
||||
|
||||
// abort filepath.Walk
|
||||
return errors.New("source found")
|
||||
|
|
@ -147,7 +153,34 @@ func sourceFromArg(arg string) (*source, error) {
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get absolute path: %w", err)
|
||||
}
|
||||
return &source{r, u}, nil
|
||||
return &source{reader: r, URL: u}, nil
|
||||
}
|
||||
|
||||
func (s source) isCode() bool {
|
||||
if !utils.IsMarkdownFile(s.URL) {
|
||||
return true
|
||||
}
|
||||
return filepath.Ext(s.URL) == "" && s.codeBlockLanguage() != ""
|
||||
}
|
||||
|
||||
func (s source) codeBlockLanguage() string {
|
||||
if ext := filepath.Ext(s.URL); ext != "" {
|
||||
return ext
|
||||
}
|
||||
return codeBlockLanguageFromContentType(s.contentType)
|
||||
}
|
||||
|
||||
func codeBlockLanguageFromContentType(contentType string) string {
|
||||
mediaType, _, err := mime.ParseMediaType(contentType)
|
||||
if err != nil {
|
||||
mediaType = strings.TrimSpace(strings.Split(contentType, ";")[0])
|
||||
}
|
||||
mediaType = strings.ToLower(mediaType)
|
||||
|
||||
if mediaType == "application/json" || strings.HasSuffix(mediaType, "+json") {
|
||||
return ".json"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// validateStyle checks if the style is a default style, if not, checks that
|
||||
|
|
@ -288,7 +321,7 @@ func executeCLI(cmd *cobra.Command, src *source, w io.Writer) error {
|
|||
baseURL = u.String() + "/"
|
||||
}
|
||||
|
||||
isCode := !utils.IsMarkdownFile(src.URL)
|
||||
isCode := src.isCode()
|
||||
|
||||
// initialize glamour
|
||||
r, err := glamour.NewTermRenderer(
|
||||
|
|
@ -303,7 +336,7 @@ func executeCLI(cmd *cobra.Command, src *source, w io.Writer) error {
|
|||
}
|
||||
|
||||
content := string(b)
|
||||
ext := filepath.Ext(src.URL)
|
||||
ext := src.codeBlockLanguage()
|
||||
if isCode {
|
||||
content = utils.WrapCodeBlock(string(b), ext)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue