154 lines
2.7 KiB
Go
154 lines
2.7 KiB
Go
|
package metabolize
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"golang.org/x/net/html"
|
||
|
"io"
|
||
|
"net/url"
|
||
|
"reflect"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
TagName = `meta`
|
||
|
htmlRegion = `head`
|
||
|
htmlTag = `meta`
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
NotStructError = fmt.Errorf(`Destination is not a struct`)
|
||
|
)
|
||
|
|
||
|
type MetaData map[string]string
|
||
|
|
||
|
type Meta struct {
|
||
|
Title string `meta:og:title`
|
||
|
Desc string `meta:og:image`
|
||
|
}
|
||
|
|
||
|
func Metabolize(doc io.Reader, obj interface{}) error {
|
||
|
data, err := ParseDocument(doc)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return Decode(data, obj)
|
||
|
}
|
||
|
|
||
|
func Decode(data MetaData, obj interface{}) error {
|
||
|
elem := reflect.ValueOf(obj).Elem()
|
||
|
if elem.Kind() != reflect.Struct {
|
||
|
return NotStructError
|
||
|
}
|
||
|
|
||
|
for i := 0; i < elem.NumField(); i++ {
|
||
|
field := elem.Type().Field(i)
|
||
|
|
||
|
fieldValue := elem.FieldByName(field.Name)
|
||
|
if !fieldValue.IsValid() {
|
||
|
continue
|
||
|
}
|
||
|
if !fieldValue.CanSet() {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
tag := field.Tag.Get(TagName)
|
||
|
if tag == "" {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
tags := strings.Split(tag, ",")
|
||
|
for _, tagItem := range tags {
|
||
|
if data[tagItem] == "" {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if fieldValue.Kind() == reflect.String {
|
||
|
val := string(data[tagItem])
|
||
|
fieldValue.SetString(val)
|
||
|
}
|
||
|
|
||
|
if fieldValue.Kind() == reflect.Bool {
|
||
|
val, err := strconv.ParseBool(data[tagItem])
|
||
|
if err != nil {
|
||
|
continue
|
||
|
}
|
||
|
fieldValue.SetBool(val)
|
||
|
}
|
||
|
|
||
|
if fieldValue.Kind() == reflect.Float64 {
|
||
|
val, err := strconv.ParseFloat(data[tagItem], 64)
|
||
|
if err != nil {
|
||
|
continue
|
||
|
}
|
||
|
fieldValue.SetFloat(val)
|
||
|
}
|
||
|
|
||
|
if fieldValue.Kind() == reflect.Int64 {
|
||
|
val, err := strconv.ParseInt(data[tagItem], 0, 64)
|
||
|
if err != nil {
|
||
|
continue
|
||
|
}
|
||
|
fieldValue.SetInt(val)
|
||
|
}
|
||
|
|
||
|
if field.Type.Name() == "URL" {
|
||
|
val, err := url.Parse(data[tagItem])
|
||
|
if err != nil {
|
||
|
continue
|
||
|
}
|
||
|
fieldValue.Set(reflect.ValueOf(*val))
|
||
|
}
|
||
|
|
||
|
if field.Type.Name() == "Time" {
|
||
|
val, err := time.Parse(time.RFC3339, data[tagItem])
|
||
|
if err != nil {
|
||
|
continue
|
||
|
}
|
||
|
fieldValue.Set(reflect.ValueOf(val))
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func ParseDocument(doc io.Reader) (MetaData, error) {
|
||
|
data := MetaData{}
|
||
|
tokenizer := html.NewTokenizer(doc)
|
||
|
for {
|
||
|
tt := tokenizer.Next()
|
||
|
if tt == html.ErrorToken {
|
||
|
if tokenizer.Err() == io.EOF {
|
||
|
return data, nil
|
||
|
}
|
||
|
return nil, tokenizer.Err()
|
||
|
}
|
||
|
|
||
|
token := tokenizer.Token()
|
||
|
|
||
|
if token.Type == html.EndTagToken && token.Data == htmlRegion {
|
||
|
return data, nil
|
||
|
}
|
||
|
|
||
|
if token.Data == htmlTag {
|
||
|
var property, content string
|
||
|
for _, attr := range token.Attr {
|
||
|
switch attr.Key {
|
||
|
case "property", "name":
|
||
|
property = strings.ToLower(attr.Val)
|
||
|
case "content":
|
||
|
content = attr.Val
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if property != "" {
|
||
|
data[strings.TrimSpace(property)] = content
|
||
|
}
|
||
|
|
||
|
}
|
||
|
}
|
||
|
return data, nil
|
||
|
}
|