Skip to content
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

chore(gnoweb): cleanup iteration on gnoweb #3379

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions gno.land/cmd/gnoweb/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@
if cfg.verbose {
level = zapcore.DebugLevel
}

var zapLogger *zap.Logger
if cfg.json {
zapLogger = log.NewZapJSONLogger(io.Out(), level)
Expand All @@ -155,30 +154,32 @@

logger := log.ZapLoggerToSlog(zapLogger)

// Setup app
appcfg := gnoweb.NewDefaultAppConfig()
appcfg.ChainID = cfg.chainid
appcfg.NodeRemote = cfg.remote
appcfg.RemoteHelp = cfg.remoteHelp
if appcfg.RemoteHelp == "" {
appcfg.RemoteHelp = appcfg.NodeRemote
}
appcfg.Analytics = cfg.analytics
appcfg.UnsafeHTML = cfg.html
appcfg.FaucetURL = cfg.faucetURL
appcfg.AssetsDir = cfg.assetsDir
if appcfg.RemoteHelp == "" {
appcfg.RemoteHelp = appcfg.NodeRemote
}

app, err := gnoweb.NewRouter(logger, appcfg)
if err != nil {
return nil, fmt.Errorf("unable to start gnoweb app: %w", err)
}

// Resolve binding address
bindaddr, err := net.ResolveTCPAddr("tcp", cfg.bind)
if err != nil {
return nil, fmt.Errorf("unable to resolve listener %q: %w", cfg.bind, err)
}

logger.Info("Running", "listener", bindaddr.String())

// Setup server
server := &http.Server{
Handler: app,
Addr: bindaddr.String(),
Expand All @@ -187,10 +188,9 @@

return func() error {
if err := server.ListenAndServe(); err != nil {
logger.Error("HTTP server stopped", " error:", err)
logger.Error("HTTP server stopped", "error", err)

Check warning on line 191 in gno.land/cmd/gnoweb/main.go

View check run for this annotation

Codecov / codecov/patch

gno.land/cmd/gnoweb/main.go#L191

Added line #L191 was not covered by tests
return commands.ExitCodeError(1)
}

return nil
}, nil
}
3 changes: 3 additions & 0 deletions gno.land/pkg/gnoweb/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ cache_dir := .cache
# Install dependencies
all: generate

test:
go test -v ./...

# Generate process
generate: css ts static

Expand Down
79 changes: 46 additions & 33 deletions gno.land/pkg/gnoweb/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,28 +31,29 @@
ChainID string
// AssetsPath is the base path to the gnoweb assets.
AssetsPath string
// AssetDir, if set, will be used for assets instead of the embedded public directory
// AssetDir, if set, will be used for assets instead of the embedded public directory.
AssetsDir string
// FaucetURL, if specified, will be the URL to which `/faucet` redirects.
FaucetURL string
// Domain is the domain used by the node.
Domain string
}

// NewDefaultAppConfig returns a new default [AppConfig]. The default sets
// 127.0.0.1:26657 as the remote node, "dev" as the chain ID and sets up Assets
// to be served on /public/.
func NewDefaultAppConfig() *AppConfig {
const defaultRemote = "127.0.0.1:26657"

return &AppConfig{
// same as Remote by default
NodeRemote: defaultRemote,
RemoteHelp: defaultRemote,
ChainID: "dev",
AssetsPath: "/public/",
Domain: "gno.land",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't we discuss this getting this from the remote genesis.json, together with the chain id?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should ideally be queried from the running node.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely want to do it. However, I didn't change any real behaviors (or really minimally, like some errors) in this PR. It's mostly a cleanup iteration with documentation and tests. So i prefer keeping this for a next PR.

}
}

var chromaStyle = mustGetStyle("friendly")
var chromaDefaultStyle = mustGetStyle("friendly")

func mustGetStyle(name string) *chroma.Style {
s := styles.Get(name)
Expand All @@ -62,15 +63,24 @@
return s
}

// NewRouter initializes the gnoweb router, with the given logger and config.
// NewRouter initializes the gnoweb router with the specified logger and configuration.
func NewRouter(logger *slog.Logger, cfg *AppConfig) (http.Handler, error) {
// Initialize RPC Client
client, err := client.NewHTTPClient(cfg.NodeRemote)
if err != nil {
return nil, fmt.Errorf("unable to create HTTP client: %w", err)
}

Check warning on line 72 in gno.land/pkg/gnoweb/app.go

View check run for this annotation

Codecov / codecov/patch

gno.land/pkg/gnoweb/app.go#L71-L72

Added lines #L71 - L72 were not covered by tests

// 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.WithExtensions(
markdown.NewHighlighting(
Expand All @@ -81,36 +91,41 @@
if cfg.UnsafeHTML {
mdopts = append(mdopts, goldmark.WithRendererOptions(mdhtml.WithXHTML(), mdhtml.WithUnsafe()))
}

md := goldmark.New(mdopts...)

client, err := client.NewHTTPClient(cfg.NodeRemote)
if err != nil {
return nil, fmt.Errorf("unable to create http client: %w", err)
// Configure WebClient
webcfg := HTMLWebClientConfig{
Markdown: md,
Highlighter: NewChromaSourceHighlighter(chroma, chromaDefaultStyle),
Domain: cfg.Domain,
UnsafeHTML: cfg.UnsafeHTML,
RPCClient: client,
}
webcli := NewWebClient(logger, client, md)

formatter := chromahtml.New(chromaOptions...)
webcli := NewHTMLClient(logger, &webcfg)
chromaStylePath := path.Join(cfg.AssetsPath, "_chroma", "style.css")

var webConfig WebHandlerConfig

webConfig.RenderClient = webcli
webConfig.Formatter = newFormatterWithStyle(formatter, chromaStyle)

// Static meta
webConfig.Meta.AssetsPath = cfg.AssetsPath
webConfig.Meta.ChromaPath = chromaStylePath
webConfig.Meta.RemoteHelp = cfg.RemoteHelp
webConfig.Meta.ChainId = cfg.ChainID
webConfig.Meta.Analytics = cfg.Analytics
// Setup StaticMetadata
staticMeta := StaticMetadata{
Domain: cfg.Domain,
AssetsPath: cfg.AssetsPath,
ChromaPath: chromaStylePath,
RemoteHelp: cfg.RemoteHelp,
ChainId: cfg.ChainID,
Analytics: cfg.Analytics,
}

// Setup main handler
webhandler := NewWebHandler(logger, webConfig)
// Configure WebHandler
webConfig := WebHandlerConfig{WebClient: webcli, Meta: staticMeta}
webhandler, err := NewWebHandler(logger, webConfig)
if err != nil {
return nil, fmt.Errorf("unable to create web handler: %w", err)
}

Check warning on line 123 in gno.land/pkg/gnoweb/app.go

View check run for this annotation

Codecov / codecov/patch

gno.land/pkg/gnoweb/app.go#L122-L123

Added lines #L122 - L123 were not covered by tests

// Setup HTTP muxer
mux := http.NewServeMux()

// Setup Webahndler along Alias Middleware
// Handle web handler with alias middleware
mux.Handle("/", AliasAndRedirectMiddleware(webhandler, cfg.Analytics))

// Register faucet URL to `/faucet` if specified
Expand All @@ -124,22 +139,20 @@
}))
}

// setup assets
// Handle Chroma CSS requests
// XXX: probably move this
mux.Handle(chromaStylePath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Setup Formatter
w.Header().Set("Content-Type", "text/css")
if err := formatter.WriteCSS(w, chromaStyle); err != nil {
logger.Error("unable to write css", "err", err)
if err := chroma.WriteCSS(w, chromaDefaultStyle); err != nil {
logger.Error("unable to write CSS", "err", err)

Check warning on line 147 in gno.land/pkg/gnoweb/app.go

View check run for this annotation

Codecov / codecov/patch

gno.land/pkg/gnoweb/app.go#L147

Added line #L147 was not covered by tests
http.NotFound(w, r)
}
}))

// Normalize assets path
assetsBase := "/" + strings.Trim(cfg.AssetsPath, "/") + "/"

// Handle assets path
assetsBase := "/" + strings.Trim(cfg.AssetsPath, "/") + "/"
if cfg.AssetsDir != "" {
logger.Debug("using assets dir instead of embed assets", "dir", cfg.AssetsDir)
logger.Debug("using assets dir instead of embedded assets", "dir", cfg.AssetsDir)

Check warning on line 155 in gno.land/pkg/gnoweb/app.go

View check run for this annotation

Codecov / codecov/patch

gno.land/pkg/gnoweb/app.go#L155

Added line #L155 was not covered by tests
mux.Handle(assetsBase, DevAssetHandler(assetsBase, cfg.AssetsDir))
} else {
mux.Handle(assetsBase, AssetHandler())
Expand Down
31 changes: 19 additions & 12 deletions gno.land/pkg/gnoweb/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ func TestRoutes(t *testing.T) {
status int
substring string
}{
{"/", ok, "Welcome"}, // assert / gives 200 (OK). assert / contains "Welcome".
{"/", ok, "Welcome"}, // Check if / returns 200 (OK) and contains "Welcome".
{"/about", ok, "blockchain"},
{"/r/gnoland/blog", ok, ""}, // whatever content
{"/r/gnoland/blog", ok, ""}, // Any content
{"/r/gnoland/blog$help", ok, "AdminSetAdminAddr"},
{"/r/gnoland/blog/", ok, "admin.gno"},
{"/r/gnoland/blog/admin.gno", ok, ">func<"},
Expand All @@ -47,12 +47,18 @@ func TestRoutes(t *testing.T) {
{"/game-of-realms", found, "/contribute"},
{"/gor", found, "/contribute"},
{"/blog", found, "/r/gnoland/blog"},
{"/404/not/found/", notFound, ""},
{"/r/not/found/", notFound, ""},
{"/404/not/found", notFound, ""},
{"/아스키문자가아닌경로", notFound, ""},
{"/%ED%85%8C%EC%8A%A4%ED%8A%B8", notFound, ""},
{"/グノー", notFound, ""},
{"/⚛️", notFound, ""},
{"/\u269B\uFE0F", notFound, ""}, // Unicode
{"/p/demo/flow/LICENSE", ok, "BSD 3-Clause"},
// Test assets
{"/public/styles.css", ok, ""},
{"/public/js/index.js", ok, ""},
{"/public/_chroma/style.css", ok, ""},
{"/public/imgs/gnoland.svg", ok, ""},
}

rootdir := gnoenv.RootDir()
Expand All @@ -66,13 +72,13 @@ func TestRoutes(t *testing.T) {

logger := log.NewTestingLogger(t)

// set the `remoteAddr` of the client to the listening address of the
// node, which is randomly assigned.
// Initialize the router with the current node's remote address
router, err := NewRouter(logger, cfg)
require.NoError(t, err)

for _, r := range routes {
t.Run(fmt.Sprintf("test route %s", r.route), func(t *testing.T) {
t.Logf("input: %q", r.route)
request := httptest.NewRequest(http.MethodGet, r.route, nil)
response := httptest.NewRecorder()
router.ServeHTTP(response, request)
Expand All @@ -84,24 +90,24 @@ func TestRoutes(t *testing.T) {

func TestAnalytics(t *testing.T) {
routes := []string{
// special realms
"/", // home
// Special realms
"/", // Home
"/about",
"/start",

// redirects
// Redirects
"/game-of-realms",
"/getting-started",
"/blog",
"/boards",

// realm, source, help page
// Realm, source, help page
"/r/gnoland/blog",
"/r/gnoland/blog/admin.gno",
"/r/demo/users:administrator",
"/r/gnoland/blog$help",

// special pages
// Special pages
"/404-not-found",
}

Expand All @@ -124,8 +130,8 @@ func TestAnalytics(t *testing.T) {

request := httptest.NewRequest(http.MethodGet, route, nil)
response := httptest.NewRecorder()

router.ServeHTTP(response, request)
fmt.Println("HELLO:", response.Body.String())
assert.Contains(t, response.Body.String(), "sa.gno.services")
})
}
Expand All @@ -142,6 +148,7 @@ func TestAnalytics(t *testing.T) {

request := httptest.NewRequest(http.MethodGet, route, nil)
response := httptest.NewRecorder()

router.ServeHTTP(response, request)
assert.NotContains(t, response.Body.String(), "sa.gno.services")
})
Expand Down
69 changes: 69 additions & 0 deletions gno.land/pkg/gnoweb/format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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")

Check warning on line 41 in gno.land/pkg/gnoweb/format.go

View check run for this annotation

Codecov / codecov/patch

gno.land/pkg/gnoweb/format.go#L38-L41

Added lines #L38 - L41 were not covered by tests
default:
lexer = lexers.Get("txt") // Unsupported file type, default to plain text.
}

if lexer == nil {
return fmt.Errorf("unsupported lexer for file %q", fileName)
}

Check warning on line 48 in gno.land/pkg/gnoweb/format.go

View check run for this annotation

Codecov / codecov/patch

gno.land/pkg/gnoweb/format.go#L47-L48

Added lines #L47 - L48 were not covered by tests

iterator, err := lexer.Tokenise(nil, string(src))
if err != nil {
return fmt.Errorf("unable to tokenise %q: %w", fileName, err)
}

Check warning on line 53 in gno.land/pkg/gnoweb/format.go

View check run for this annotation

Codecov / codecov/patch

gno.land/pkg/gnoweb/format.go#L52-L53

Added lines #L52 - L53 were not covered by tests

if err := f.Formatter.Format(w, f.style, iterator); err != nil {
return fmt.Errorf("unable to format source file %q: %w", fileName, err)
}

Check warning on line 57 in gno.land/pkg/gnoweb/format.go

View check run for this annotation

Codecov / codecov/patch

gno.land/pkg/gnoweb/format.go#L56-L57

Added lines #L56 - L57 were not covered by tests

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

Check warning on line 68 in gno.land/pkg/gnoweb/format.go

View check run for this annotation

Codecov / codecov/patch

gno.land/pkg/gnoweb/format.go#L66-L68

Added lines #L66 - L68 were not covered by tests
}
Loading
Loading