From 90efc4596512ba209018503d0717ba868228ff4a Mon Sep 17 00:00:00 2001 From: alexiscolin Date: Fri, 17 Jan 2025 16:27:32 +0900 Subject: [PATCH 1/3] fix: gnoweb titles id - toc --- gno.land/pkg/gnoweb/app.go | 4 ++++ gno.land/pkg/gnoweb/webclient_html.go | 13 ------------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/gno.land/pkg/gnoweb/app.go b/gno.land/pkg/gnoweb/app.go index 516d3b92186..94978223f64 100644 --- a/gno.land/pkg/gnoweb/app.go +++ b/gno.land/pkg/gnoweb/app.go @@ -16,6 +16,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/parser" mdhtml "github.com/yuin/goldmark/renderer/html" ) @@ -84,6 +85,9 @@ func NewRouter(logger *slog.Logger, cfg *AppConfig) (http.Handler, error) { // Configure Goldmark markdown parser mdopts := []goldmark.Option{ + goldmark.WithParserOptions( + parser.WithAutoHeadingID(), + ), goldmark.WithExtensions( markdown.NewHighlighting( markdown.WithFormatOptions(chromaOptions...), diff --git a/gno.land/pkg/gnoweb/webclient_html.go b/gno.land/pkg/gnoweb/webclient_html.go index ffe2238df98..33543f69915 100644 --- a/gno.land/pkg/gnoweb/webclient_html.go +++ b/gno.land/pkg/gnoweb/webclient_html.go @@ -13,7 +13,6 @@ import ( "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" "github.com/yuin/goldmark" - "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/text" ) @@ -25,18 +24,6 @@ type HTMLWebClientConfig struct { Markdown goldmark.Markdown } -// NewDefaultHTMLWebClientConfig initializes a WebClientConfig with default settings. -// It sets up goldmark Markdown parsing options and default domain and highlighter. -func NewDefaultHTMLWebClientConfig(client *client.RPCClient) *HTMLWebClientConfig { - mdopts := []goldmark.Option{goldmark.WithParserOptions(parser.WithAutoHeadingID())} - return &HTMLWebClientConfig{ - Domain: "gno.land", - Highlighter: &noopFormat{}, - Markdown: goldmark.New(mdopts...), - RPCClient: client, - } -} - type HTMLWebClient struct { domain string logger *slog.Logger From 5a1d0fc9356c6917c4e8a52a46fe36792991ed5a Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Fri, 17 Jan 2025 11:05:00 +0100 Subject: [PATCH 2/3] feat: centralize format logic inside webclient Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gno.land/pkg/gnoweb/app.go | 62 +++------------- gno.land/pkg/gnoweb/format.go | 69 ----------------- gno.land/pkg/gnoweb/webclient_html.go | 103 +++++++++++++++++++++++--- 3 files changed, 100 insertions(+), 134 deletions(-) delete mode 100644 gno.land/pkg/gnoweb/format.go diff --git a/gno.land/pkg/gnoweb/app.go b/gno.land/pkg/gnoweb/app.go index 94978223f64..455a9aafaf1 100644 --- a/gno.land/pkg/gnoweb/app.go +++ b/gno.land/pkg/gnoweb/app.go @@ -7,16 +7,9 @@ import ( "path" "strings" - markdown "github.com/yuin/goldmark-highlighting/v2" - - "github.com/alecthomas/chroma/v2" - chromahtml "github.com/alecthomas/chroma/v2/formatters/html" - "github.com/alecthomas/chroma/v2/styles" "github.com/gnolang/gno/gno.land/pkg/gnoweb/components" "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" "github.com/yuin/goldmark" - "github.com/yuin/goldmark/extension" - "github.com/yuin/goldmark/parser" mdhtml "github.com/yuin/goldmark/renderer/html" ) @@ -56,16 +49,6 @@ func NewDefaultAppConfig() *AppConfig { } } -var chromaDefaultStyle = mustGetStyle("friendly") - -func mustGetStyle(name string) *chroma.Style { - s := styles.Get(name) - if s == nil { - panic("unable to get chroma style") - } - return s -} - // NewRouter initializes the gnoweb router with the specified logger and configuration. func NewRouter(logger *slog.Logger, cfg *AppConfig) (http.Handler, error) { // Initialize RPC Client @@ -74,45 +57,18 @@ func NewRouter(logger *slog.Logger, cfg *AppConfig) (http.Handler, error) { return nil, fmt.Errorf("unable to create HTTP client: %w", err) } - // Configure Chroma highlighter - chromaOptions := []chromahtml.Option{ - chromahtml.WithLineNumbers(true), - chromahtml.WithLinkableLineNumbers(true, "L"), - chromahtml.WithClasses(true), - chromahtml.ClassPrefix("chroma-"), - } - chroma := chromahtml.New(chromaOptions...) - - // Configure Goldmark markdown parser - mdopts := []goldmark.Option{ - goldmark.WithParserOptions( - parser.WithAutoHeadingID(), - ), - goldmark.WithExtensions( - markdown.NewHighlighting( - markdown.WithFormatOptions(chromaOptions...), - ), - extension.Table, - ), - } + // Setup web client HTML + webcfg := NewDefaultHTMLWebClientConfig(client) + webcfg.Domain = cfg.Domain if cfg.UnsafeHTML { - mdopts = append(mdopts, goldmark.WithRendererOptions(mdhtml.WithXHTML(), mdhtml.WithUnsafe())) - } - md := goldmark.New(mdopts...) - - // Configure WebClient - webcfg := HTMLWebClientConfig{ - Markdown: md, - Highlighter: NewChromaSourceHighlighter(chroma, chromaDefaultStyle), - Domain: cfg.Domain, - UnsafeHTML: cfg.UnsafeHTML, - RPCClient: client, + webcfg.GoldmarkOptions = append(webcfg.GoldmarkOptions, goldmark.WithRendererOptions( + mdhtml.WithXHTML(), mdhtml.WithUnsafe(), + )) } - - webcli := NewHTMLClient(logger, &webcfg) - chromaStylePath := path.Join(cfg.AssetsPath, "_chroma", "style.css") + webcli := NewHTMLClient(logger, webcfg) // Setup StaticMetadata + chromaStylePath := path.Join(cfg.AssetsPath, "_chroma", "style.css") staticMeta := StaticMetadata{ Domain: cfg.Domain, AssetsPath: cfg.AssetsPath, @@ -150,7 +106,7 @@ func NewRouter(logger *slog.Logger, cfg *AppConfig) (http.Handler, error) { // XXX: probably move this elsewhere mux.Handle(chromaStylePath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/css") - if err := chroma.WriteCSS(w, chromaDefaultStyle); err != nil { + if err := webcli.WriteFormatterCSS(w); err != nil { logger.Error("unable to write CSS", "err", err) http.NotFound(w, r) } diff --git a/gno.land/pkg/gnoweb/format.go b/gno.land/pkg/gnoweb/format.go deleted file mode 100644 index 67911bfa985..00000000000 --- a/gno.land/pkg/gnoweb/format.go +++ /dev/null @@ -1,69 +0,0 @@ -package gnoweb - -import ( - "fmt" - "io" - "path/filepath" - "strings" - - "github.com/alecthomas/chroma/v2" - "github.com/alecthomas/chroma/v2/formatters/html" - "github.com/alecthomas/chroma/v2/lexers" -) - -// FormatSource defines the interface for formatting source code. -type FormatSource interface { - Format(w io.Writer, fileName string, file []byte) error -} - -// ChromaSourceHighlighter implements the Highlighter interface using the Chroma library. -type ChromaSourceHighlighter struct { - *html.Formatter - style *chroma.Style -} - -// NewChromaSourceHighlighter constructs a new ChromaHighlighter with the given formatter and style. -func NewChromaSourceHighlighter(formatter *html.Formatter, style *chroma.Style) FormatSource { - return &ChromaSourceHighlighter{Formatter: formatter, style: style} -} - -// Format applies syntax highlighting to the source code using Chroma. -func (f *ChromaSourceHighlighter) Format(w io.Writer, fileName string, src []byte) error { - var lexer chroma.Lexer - - // Determine the lexer to be used based on the file extension. - switch strings.ToLower(filepath.Ext(fileName)) { - case ".gno": - lexer = lexers.Get("go") - case ".md": - lexer = lexers.Get("markdown") - case ".mod": - lexer = lexers.Get("gomod") - default: - lexer = lexers.Get("txt") // Unsupported file type, default to plain text. - } - - if lexer == nil { - return fmt.Errorf("unsupported lexer for file %q", fileName) - } - - iterator, err := lexer.Tokenise(nil, string(src)) - if err != nil { - return fmt.Errorf("unable to tokenise %q: %w", fileName, err) - } - - if err := f.Formatter.Format(w, f.style, iterator); err != nil { - return fmt.Errorf("unable to format source file %q: %w", fileName, err) - } - - return nil -} - -// noopFormat is a no-operation highlighter that writes the source code as-is. -type noopFormat struct{} - -// Format writes the source code to the writer without any formatting. -func (f *noopFormat) Format(w io.Writer, fileName string, src []byte) error { - _, err := w.Write(src) - return err -} diff --git a/gno.land/pkg/gnoweb/webclient_html.go b/gno.land/pkg/gnoweb/webclient_html.go index 33543f69915..d856c6f87a0 100644 --- a/gno.land/pkg/gnoweb/webclient_html.go +++ b/gno.land/pkg/gnoweb/webclient_html.go @@ -8,39 +8,83 @@ import ( "path/filepath" "strings" + "github.com/alecthomas/chroma/v2" + chromahtml "github.com/alecthomas/chroma/v2/formatters/html" + "github.com/alecthomas/chroma/v2/lexers" + "github.com/alecthomas/chroma/v2/styles" md "github.com/gnolang/gno/gno.land/pkg/gnoweb/markdown" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" // for error types "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" "github.com/yuin/goldmark" + markdown "github.com/yuin/goldmark-highlighting/v2" + "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/text" ) +var chromaDefaultStyle = styles.Get("friendly") + type HTMLWebClientConfig struct { - Domain string - UnsafeHTML bool - RPCClient *client.RPCClient - Highlighter FormatSource - Markdown goldmark.Markdown + Domain string + RPCClient *client.RPCClient + ChromaStyle *chroma.Style + ChromaHTMLOptions []chromahtml.Option + GoldmarkOptions []goldmark.Option +} + +// NewDefaultHTMLWebClientConfig initializes a WebClientConfig with default settings. +// It sets up goldmark Markdown parsing options and default domain and highlighter. +func NewDefaultHTMLWebClientConfig(client *client.RPCClient) *HTMLWebClientConfig { + chromaOptions := []chromahtml.Option{ + chromahtml.WithLineNumbers(true), + chromahtml.WithLinkableLineNumbers(true, "L"), + chromahtml.WithClasses(true), + chromahtml.ClassPrefix("chroma-"), + } + + goldmarkOptions := []goldmark.Option{ + goldmark.WithParserOptions(parser.WithAutoHeadingID()), + goldmark.WithExtensions( + markdown.NewHighlighting( + markdown.WithFormatOptions(chromaOptions...), + ), + extension.Table, + ), + } + + return &HTMLWebClientConfig{ + Domain: "gno.land", + GoldmarkOptions: goldmarkOptions, + ChromaHTMLOptions: chromaOptions, + ChromaStyle: chromaDefaultStyle, + RPCClient: client, + } } type HTMLWebClient struct { + Markdown goldmark.Markdown + Formatter *chromahtml.Formatter + domain string logger *slog.Logger client *client.RPCClient - md goldmark.Markdown - highlighter FormatSource + chromaStyle *chroma.Style } // NewHTMLClient creates a new instance of WebClient. // It requires a configured logger and WebClientConfig. func NewHTMLClient(log *slog.Logger, cfg *HTMLWebClientConfig) *HTMLWebClient { return &HTMLWebClient{ + // XXX: Possibly consider exporting this in a single interface logic. + // For now it's easier to manager all this in one place + Markdown: goldmark.New(cfg.GoldmarkOptions...), + Formatter: chromahtml.New(cfg.ChromaHTMLOptions...), + logger: log, domain: cfg.Domain, client: cfg.RPCClient, - md: cfg.Markdown, - highlighter: cfg.Highlighter, + chromaStyle: cfg.ChromaStyle, } } @@ -95,7 +139,7 @@ func (s *HTMLWebClient) SourceFile(w io.Writer, path, fileName string) (*FileMet } // Use Chroma for syntax highlighting - if err := s.highlighter.Format(w, fileName, source); err != nil { + if err := s.FormatSource(w, fileName, source); err != nil { return nil, err } @@ -139,8 +183,8 @@ func (s *HTMLWebClient) RenderRealm(w io.Writer, pkgPath string, args string) (* } // Use Goldmark for Markdown parsing - doc := s.md.Parser().Parse(text.NewReader(rawres)) - if err := s.md.Renderer().Render(w, rawres, doc); err != nil { + doc := s.Markdown.Parser().Parse(text.NewReader(rawres)) + if err := s.Markdown.Renderer().Render(w, rawres, doc); err != nil { return nil, fmt.Errorf("unable to render realm %q: %w", data, err) } @@ -175,3 +219,38 @@ func (s *HTMLWebClient) query(qpath string, data []byte) ([]byte, error) { return qres.Response.Data, nil } + +func (s *HTMLWebClient) FormatSource(w io.Writer, fileName string, src []byte) error { + var lexer chroma.Lexer + + // Determine the lexer to be used based on the file extension. + switch strings.ToLower(filepath.Ext(fileName)) { + case ".gno": + lexer = lexers.Get("go") + case ".md": + lexer = lexers.Get("markdown") + case ".mod": + lexer = lexers.Get("gomod") + default: + lexer = lexers.Get("txt") // Unsupported file type, default to plain text. + } + + if lexer == nil { + return fmt.Errorf("unsupported lexer for file %q", fileName) + } + + iterator, err := lexer.Tokenise(nil, string(src)) + if err != nil { + return fmt.Errorf("unable to tokenise %q: %w", fileName, err) + } + + if err := s.Formatter.Format(w, s.chromaStyle, iterator); err != nil { + return fmt.Errorf("unable to format source file %q: %w", fileName, err) + } + + return nil +} + +func (s *HTMLWebClient) WriteFormatterCSS(w io.Writer) error { + return s.Formatter.WriteCSS(w, s.chromaStyle) +} From 5d9a39ec68f8bb10fecdd3b45435abf5100034b4 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Fri, 17 Jan 2025 11:06:15 +0100 Subject: [PATCH 3/3] feat: add toc test Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gno.land/pkg/gnoweb/app_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gno.land/pkg/gnoweb/app_test.go b/gno.land/pkg/gnoweb/app_test.go index 9f8f87b99b1..6fb69c6d984 100644 --- a/gno.land/pkg/gnoweb/app_test.go +++ b/gno.land/pkg/gnoweb/app_test.go @@ -59,6 +59,8 @@ func TestRoutes(t *testing.T) { {"/public/js/index.js", ok, ""}, {"/public/_chroma/style.css", ok, ""}, {"/public/imgs/gnoland.svg", ok, ""}, + // Test Toc + {"/", ok, `href="#learn-about-gnoland"`}, } rootdir := gnoenv.RootDir()