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:
Asish Kumar 2026-05-25 03:17:45 +05:30
commit 6cb86a1338
No known key found for this signature in database
GPG key ID: 0EF072B1E0BA6FBA
4 changed files with 95 additions and 9 deletions

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
View file

@ -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)
}