-
Notifications
You must be signed in to change notification settings - Fork 456
feat(gnoweb): serve realm pages as markdown via Accept negotiation #5794
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
e7ea244
a78ae9a
22b6e20
5e04188
9938c09
a8dbdfa
589d197
4ab2753
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package components | ||
|
|
||
| import "bytes" | ||
|
|
||
| // MarkdownViewType marks a View whose content is raw markdown, to be served | ||
| // verbatim as text/markdown without the HTML page layout. | ||
| const MarkdownViewType ViewType = "markdown-view" | ||
|
|
||
| // MarkdownView returns a View that renders the given content as-is. The handler | ||
| // layer recognizes this view type and serves it with a text/markdown | ||
| // Content-Type, bypassing IndexLayout. | ||
| func MarkdownView(content []byte) *View { | ||
| return &View{ | ||
| Type: MarkdownViewType, | ||
| Component: NewReaderComponent(bytes.NewReader(content)), | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package components_test | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "testing" | ||
|
|
||
| "github.com/gnolang/gno/gno.land/pkg/gnoweb/components" | ||
| ) | ||
|
|
||
| func TestMarkdownView(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| content := []byte("# Title\n\nbody text\n") | ||
| v := components.MarkdownView(content) | ||
|
|
||
| if v.Type != components.MarkdownViewType { | ||
| t.Fatalf("Type = %q, want %q", v.Type, components.MarkdownViewType) | ||
| } | ||
|
|
||
| var buf bytes.Buffer | ||
| if err := v.Render(&buf); err != nil { | ||
| t.Fatalf("Render returned error: %v", err) | ||
| } | ||
| if buf.String() != string(content) { | ||
| t.Fatalf("Render wrote %q, want verbatim %q", buf.String(), string(content)) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package gnoweb | ||
|
|
||
| import ( | ||
| "mime" | ||
| "strconv" | ||
| "strings" | ||
| ) | ||
|
|
||
| // negotiatesMarkdown reports whether the client explicitly accepts markdown. | ||
| // | ||
| // It returns true only when "text/markdown" (or the alias "text/x-markdown") | ||
| // is named explicitly in the Accept header and not refused with an explicit | ||
| // q=0. It never matches the "*/*" or "text/*" wildcards, so browsers — which | ||
| // always accept "*/*" — continue to receive HTML. | ||
| func negotiatesMarkdown(accept string) bool { | ||
| for _, part := range strings.Split(accept, ",") { | ||
| mediaType, params, err := mime.ParseMediaType(strings.TrimSpace(part)) | ||
| if err != nil { | ||
| continue | ||
| } | ||
| if mediaType != "text/markdown" && mediaType != "text/x-markdown" { | ||
| continue | ||
| } | ||
| if q, ok := params["q"]; ok { | ||
| if v, err := strconv.ParseFloat(q, 64); err == nil && v <= 0 { | ||
| continue | ||
| } | ||
| } | ||
| return true | ||
| } | ||
| return false | ||
| } | ||
|
Comment on lines
+15
to
+32
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
repro# from a local clone of gnolang/gno:
gh pr checkout 5794 -R gnolang/gno
go test ./gno.land/pkg/gnoweb/ -run 'TestNegotiatesMarkdown/markdown_present_with_non-zero_q_among_others' -v
(AI Agent) |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package gnoweb | ||
|
|
||
| import "testing" | ||
|
|
||
| func TestNegotiatesMarkdown(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| cases := []struct { | ||
| name string | ||
| accept string | ||
| want bool | ||
| }{ | ||
| {"empty", "", false}, | ||
| {"plain markdown", "text/markdown", true}, | ||
| {"x-markdown alias", "text/x-markdown", true}, | ||
| {"case insensitive", "TEXT/Markdown", true}, | ||
| {"markdown with charset param", "text/markdown; charset=utf-8", true}, | ||
| {"markdown q half", "text/markdown;q=0.5", true}, | ||
| {"markdown q one", "text/markdown;q=1.0", true}, | ||
| {"markdown q zero", "text/markdown;q=0", false}, | ||
| {"markdown q zero point zero", "text/markdown;q=0.0", false}, | ||
| {"markdown negative q", "text/markdown;q=-1", false}, | ||
| {"markdown malformed q", "text/markdown;q=abc", true}, | ||
| {"html then markdown", "text/html, text/markdown", true}, | ||
| {"browser accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", false}, | ||
| {"claude-code webfetch accept", "text/markdown, text/html, */*", true}, | ||
| {"wildcard only", "*/*", false}, | ||
| {"text wildcard", "text/*", false}, | ||
| {"json", "application/json", false}, | ||
| {"surrounding spaces", " text/markdown ", true}, | ||
| {"markdown present with non-zero q among others", "text/html;q=0.9, text/markdown;q=0.8", true}, | ||
| } | ||
|
|
||
| for _, tc := range cases { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| t.Parallel() | ||
| if got := negotiatesMarkdown(tc.accept); got != tc.want { | ||
| t.Errorf("negotiatesMarkdown(%q) = %v, want %v", tc.accept, got, tc.want) | ||
| } | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This write serves realm
Render()bytes verbatim (the HTML path sanitizes through goldmark) and gnoweb sets nonosniffanywhere. AddX-Content-Type-Options: nosniffhere.(AI Agent)