/* * Amsterdam Web Communities System * Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ // Package ui holds the support for the Amsterdam user interface, wrapping Echo and Jet templates. package ui import ( "bytes" "embed" "fmt" "io" "io/fs" "net/http" "os" "strings" "git.erbosoft.com/amy/amsterdam/config" "github.com/labstack/echo/v4" log "github.com/sirupsen/logrus" "golang.org/x/net/html" ) //go:embed static/* var static_data embed.FS //go:embed resources/* var static_resources embed.FS // external_resources is the link to the external resource path. var external_resources fs.FS = nil func setupResources() { // Open the external resource path. rpath := config.GlobalConfig.ExPath(config.GlobalConfig.Resources.ExternalResourcePath) if rpath != "" { finfo, err := os.Stat(rpath) if err == nil { if finfo.IsDir() { root, err := os.OpenRoot(rpath) if err != nil { panic(err) } external_resources = root.FS() } else { log.Errorf("external resource path \"%s\" is not a directory, ignored", rpath) } } else { log.Errorf("external resource path \"%s\" is not valid, ignored (%v)", rpath, err) } } } // AmStaticFileHandler returns a handler for the files in the static embedded filesystem. func AmStaticFileHandler() echo.HandlerFunc { fsys, err := fs.Sub(static_data, "static") if err != nil { panic(err) } return echo.WrapHandler(http.StripPrefix("/static/", http.FileServer(http.FS(fsys)))) } // extractPlainText extracts all plain text from a HTML tree node. func extractPlainText(n *html.Node) string { var sb strings.Builder var walk func(*html.Node) walk = func(n *html.Node) { if n.Type == html.TextNode { sb.WriteString(n.Data) } for c := n.FirstChild; c != nil; c = c.NextSibling { walk(c) } } walk(n) return sb.String() } // extractInnerHTML extracts the inner HTML from a HTML tree node. func extractInnerHTML(n *html.Node) string { var buf bytes.Buffer for c := n.FirstChild; c != nil; c = c.NextSibling { html.Render(&buf, c) } return buf.String() } // breakUpHTML extracts the title and body from an HTML page. func breakUpHTML(r io.Reader) (string, string, error) { doc, err := html.Parse(r) if err != nil { return "", "", err } title := "" body := "" var traverse func(*html.Node) traverse = func(n *html.Node) { if n.Type == html.ElementNode { switch n.Data { case "title": title = extractPlainText(n) case "body": body = extractInnerHTML(n) } } for c := n.FirstChild; c != nil; c = c.NextSibling { traverse(c) } } traverse(doc) return title, body, nil } // AmLoadHTMLResource loads an HTML resource and splits it into title and body. func AmLoadHTMLResource(resourceName string) (string, string, error) { var f fs.File = nil var err error if external_resources != nil { f, err = external_resources.Open(resourceName) if err != nil { f = nil pe := err.(*fs.PathError) if pe.Err == os.ErrInvalid || pe.Err == os.ErrNotExist { err = nil } } if err != nil { return "", "", err } } if f == nil { f, err = static_resources.Open(fmt.Sprintf("resources/%s", resourceName)) if err != nil { return "", "", err } } title, body, err := breakUpHTML(f) f.Close() return title, body, err } /* AmStaticFramePage generates a handler that will serve up data from an external filesystem "framed" inside * the frame with the template engine. * Parameters: * staticFS - The filesystem to serve from. * prefix - The prefix to be stripped off pathnames to feed to the filesystem. * Returns: * An AmPageFunc suitable for wrapping and adding to the Echo handlers. */ func AmStaticFramePage(staticFS fs.FS, prefix string) AmPageFunc { return func(ctxt AmContext) (string, any) { // Cut the prefix off the path. fname := ctxt.URLPath() if strings.HasPrefix(fname, prefix) { fname = fname[len(prefix):] } else { return "error", "invalid path name" } // Extract the basic MIME type. mtype := mimeTypeFromFilename(fname) p := strings.Index(mtype, ";") if p >= 0 { mtype = mtype[:p] } // Decide from there how to render it. ctxt.VarMap().Set("mimeType", mtype) switch mtype { case "text/html": f, err := staticFS.Open(fname) if err != nil { return "error", err } defer f.Close() title, body, err := breakUpHTML(f) if err != nil { return "error", err } ctxt.SetFrameTitle(title) ctxt.VarMap().Set("title", title) ctxt.VarMap().Set("data", body) return "framed", "extern.jet" } return "error", "Unknown MIME Type: " + mtype } }