fiber-fragments/fragment.go

295 строки
5.8 KiB
Go

package fragments
import (
"bytes"
"strconv"
"strings"
"sync"
"time"
"github.com/PuerkitoBio/goquery"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/valyala/fasthttp"
"golang.org/x/net/html"
"golang.org/x/net/html/atom"
)
// HtmlFragment is representation of HTML fragments.
type HtmlFragment struct {
doc *goquery.Document
sync.RWMutex
}
// NewHtmlFragment creates a new fragment of HTML.
func NewHtmlFragment(root *html.Node) (*HtmlFragment, error) {
h := new(HtmlFragment)
h.doc = goquery.NewDocumentFromNode(root)
return h, nil
}
// Document get the full document representation
// of the HTML fragment.
func (h *HtmlFragment) Fragment() *goquery.Document {
return h.doc
}
// Fragments is returning the selection of fragments
// from an HTML page.
func (h *HtmlFragment) Fragments() (map[string]*Fragment, error) {
h.RLock()
defer h.RUnlock()
scripts := h.doc.Find("head script[type=fragment]")
fragments := h.doc.Find("fragment").AddSelection(scripts)
ff := make(map[string]*Fragment)
fragments.Each(func(i int, s *goquery.Selection) {
f := FromSelection(s)
if !f.deferred {
ff[f.ID()] = f
}
})
return ff, nil
}
// Html creates the HTML output of the created document.
func (h *HtmlFragment) Html() (string, error) {
h.RLock()
defer h.RUnlock()
html, err := h.doc.Html()
if err != nil {
return "", err
}
return html, nil
}
// AppendHead ...
func (d *HtmlFragment) AppendHead(ns ...*html.Node) {
head := d.doc.Find("head")
head.AppendNodes(ns...)
}
// Fragment is a <fragment> in the <header> or <body>
// of a HTML page.
type Fragment struct {
deferred bool
fallback string
method string
primary bool
src string
timeout int64
id string
ref string
statusCode int
head []*html.Node
f *HtmlFragment
s *goquery.Selection
}
// FromSelection creates a new fragment from a
// fragment selection in the DOM.
func FromSelection(s *goquery.Selection) *Fragment {
f := new(Fragment)
f.s = s
src, _ := s.Attr("src")
f.src = src
fallback, _ := s.Attr("fallback")
f.fallback = fallback
method, _ := s.Attr("method")
f.method = method
timeout, ok := s.Attr("timeout")
if !ok {
timeout = "60"
}
t, _ := strconv.ParseInt(timeout, 10, 64)
f.timeout = t
id, ok := s.Attr("id")
if !ok {
id = uuid.New().String()
}
f.id = id
ref, _ := s.Attr("ref")
f.ref = ref
deferred, ok := s.Attr("deferred")
f.deferred = ok && strings.ToUpper(deferred) != "FALSE"
primary, ok := s.Attr("primary")
f.primary = ok && strings.ToUpper(primary) != "FALSE"
f.head = make([]*html.Node, 0)
return f
}
// Src is the URL for the fragment.
func (f *Fragment) Src() string {
return f.src
}
// Fallback is the fallback URL for the fragment.
func (f *Fragment) Fallback() string {
return f.fallback
}
// Timeout is the timeout for fetching the fragment.
func (f *Fragment) Timeout() time.Duration {
return time.Duration(f.timeout) * time.Second
}
// Method is the HTTP method to use for fetching the fragment.
func (f *Fragment) Method() string {
return f.method
}
// Element is a pointer to the selected element in the DOM.
func (f *Fragment) Element() *goquery.Selection {
return f.s
}
// Deferred is deferring the fetching to the browser.
func (f *Fragment) Deferred() bool {
return f.deferred
}
// Primary denotes a fragment as responsible for setting
// the response code of the entire HTML page.
func (f *Fragment) Primary() bool {
return f.primary
}
// Links returns the new nodes that go in the head via
// the LINK HTTP header entity.
func (f *Fragment) Links() []*html.Node {
return f.head
}
// Ref represents the reference to another fragment
func (f *Fragment) Ref() string {
return f.ref
}
// ID represents a unique id for the fragment
func (f *Fragment) ID() string {
return f.id
}
// HtmlFragment returns embedded fragments of HTML.
func (f *Fragment) HtmlFragment() *HtmlFragment {
return f.f
}
// Resolve is resolving all needed data, setting headers
// and the status code.
func (f *Fragment) Resolve() ResolverFunc {
return func(c *fiber.Ctx, cfg Config) error {
err := f.do(c, cfg, f.src)
if err == nil {
return err
}
if err != fasthttp.ErrTimeout {
return err
}
err = f.do(c, cfg, f.fallback)
if err != nil {
return err
}
return nil
}
}
func (f *Fragment) do(c *fiber.Ctx, cfg Config, src string) error {
req := fasthttp.AcquireRequest()
res := fasthttp.AcquireResponse()
defer fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(res)
c.Request().CopyTo(req)
uri := fasthttp.AcquireURI()
defer fasthttp.ReleaseURI(uri)
if err := uri.Parse(nil, []byte(src)); err != nil {
return err
}
if len(uri.Host()) == 0 {
uri.SetHost(cfg.DefaultHost)
}
req.SetRequestURI(uri.String())
req.Header.Del(fiber.HeaderConnection)
t := f.Timeout()
if err := client.DoTimeout(req, res, t); err != nil {
return err
}
res = cfg.FilterResponse(res)
f.statusCode = res.StatusCode()
// if res.StatusCode() != http.StatusOK {
// // TODO: wrap in custom error, to not replace
// return fmt.Errorf("resolve: could not resolve fragment at %s", f.Src())
// }
res.Header.Del(fiber.HeaderConnection)
contentEncoding := res.Header.Peek("Content-Encoding")
body := res.Body()
var err error
if bytes.EqualFold(contentEncoding, []byte("gzip")) {
body, err = res.BodyGunzip()
if err != nil {
return cfg.ErrorHandler(c, err)
}
}
h := Header(string(res.Header.Peek("link")))
nodes := CreateNodes(h.Links())
f.head = append(f.head, nodes...)
root := &html.Node{
Type: html.ElementNode,
DataAtom: atom.Body,
Data: "body",
}
ns, err := html.ParseFragment(bytes.NewReader(body), root)
if err != nil {
return err
}
for _, n := range ns {
root.AppendChild(n)
}
doc, err := NewHtmlFragment(root)
if err != nil {
return nil
}
f.f = doc
return nil
}