diff --git a/github.go b/github.go index fe862e3..326e159 100644 --- a/github.go +++ b/github.go @@ -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 } } diff --git a/gitlab.go b/gitlab.go index 68256be..5b3cb5f 100644 --- a/gitlab.go +++ b/gitlab.go @@ -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 } } diff --git a/glow_test.go b/glow_test.go index 8743be2..7353a17 100644 --- a/glow_test.go +++ b/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") + } +} diff --git a/main.go b/main.go index b31ca15..7e167c2 100644 --- a/main.go +++ b/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) }