package app
import (
"encoding/json"
"strconv"
"strings"
"github.com/krazywarez/devianter"
"golang.org/x/net/html"
)
// ParseComments renders a comment thread, nesting replies under the comment they
// answer. It returns a placeholder message rather than failing if the upstream
// fetch errored.
func (s skunkyart) ParseComments(c devianter.Comments, daError devianter.Error) string {
if daError.RAW != nil {
return "Failed to fetch comments :("
}
var cmmts strings.Builder
replied := make(map[int]string)
cmmts.WriteString("Comments: ")
cmmts.WriteString(strconv.Itoa(c.Total))
cmmts.WriteString("")
for _, x := range c.Thread {
replied[x.ID] = x.User.Username
cmmts.WriteString(`
\n")
}
cmmts.WriteString(s.NavBase(DeviationList{
Pages: 0,
More: c.HasMore,
}))
cmmts.WriteString("")
return cmmts.String()
}
// DeviationList renders devs as an HTML grid, or as an Atom feed when the
// request asked for one and allowAtom permits it. NSFW entries are dropped
// unless the instance allows them. Passing content adds a navigation bar.
func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, content ...DeviationList) string {
if s.Atom && s.Page > 1 {
s.ReturnHTTPError(400)
return ""
}
var list, listContent strings.Builder
for i, l := 0, len(devs); i < l; i++ {
data := &devs[i]
if preview, fullview := ParseMedia(s.Host, data.Media, 320), ParseMedia(s.Host, data.Media); !data.NSFW || CFG.Nsfw {
if allowAtom && s.Atom {
s.Writer.Header().Add("Content-Type", "application/atom+xml")
id := strconv.Itoa(data.ID)
listContent.WriteString(``)
listContent.WriteString(data.Author.Username)
listContent.WriteString(``)
listContent.WriteString(data.Title)
listContent.WriteString(``)
listContent.WriteString(id)
listContent.WriteString(``)
listContent.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700"))
listContent.WriteString(``)
listContent.WriteString(``)
listContent.WriteString(data.Title)
listContent.WriteString(`
")
if content != nil {
list.WriteString(s.NavBase(content[0]))
}
}
return list.String()
}
/* DESCRIPTION/COMMENT PARSER */
// text is one styled run within a description: the rendered HTML, the raw source
// it came from, and the offsets it spans in the original block.
type text struct {
Txt string
TxtRaw string
From int
To int
}
// ParseDescription renders a DeviantArt description into HTML, handling both the
// Draft.js-style JSON payload and the plain HTML markup DeviantArt returns, and
// rewriting embedded links and artwork references to point at this instance.
// host is the request's scheme and host, as taken by URLBuilder.
//
// TODO: rewrite this whole mess.
func ParseDescription(host string, dscr devianter.Text) string {
var parsedDescription strings.Builder
TagBuilder := func(content string, tags ...string) string {
l := len(tags)
for x := range l {
var htm strings.Builder
htm.WriteString("<")
htm.WriteString(tags[x])
htm.WriteString(">")
htm.WriteString(content)
htm.WriteString("")
htm.WriteString(tags[x])
htm.WriteString(">")
content = htm.String()
}
return content
}
DeleteTrackingFromURL := func(url string) string {
if len(url) > 42 && url[:42] == "https://www.deviantart.com/users/outgoing?" {
url = url[42:]
}
return url
}
if description, dl := dscr.Html.Markup, len(dscr.Html.Markup); dl != 0 &&
description[0] == '{' &&
description[dl-1] == '}' {
var descr struct {
Blocks []struct {
Text string `json:"text"`
Type string `json:"type"`
InlineStyleRanges []struct {
Offset int `json:"offset"`
Length int `json:"length"`
Style string `json:"style"`
} `json:"inlineStyleRanges"`
EntityRanges []struct {
Offset int `json:"offset"`
Length int `json:"length"`
Key int `json:"key"`
} `json:"entityRanges"`
Data struct {
TextAlignment string `json:"textAlignment"`
} `json:"data"`
} `json:"blocks"`
EntityMap map[string]struct {
Type string `json:"type"`
Data struct {
URL string `json:"url"`
Config struct {
// "aligment" is DeviantArt's own spelling; do not correct it.
Aligment string `json:"aligment"`
Width int `json:"width"`
} `json:"config"`
Data devianter.Deviation `json:"data"`
} `json:"data"`
} `json:"entityMap"`
}
e := json.Unmarshal([]byte(description), &descr)
try(e)
entities := make(map[int]devianter.Deviation)
urls := make(map[int]string)
for n, x := range descr.EntityMap {
num, _ := strconv.Atoi(n)
if x.Data.URL != "" {
urls[num] = DeleteTrackingFromURL(x.Data.URL)
}
entities[num] = x.Data.Data
}
for _, x := range descr.Blocks {
Styles := make([]text, len(x.InlineStyleRanges))
if len(x.InlineStyleRanges) != 0 {
var tags = make(map[int][]string)
for n, rngs := range x.InlineStyleRanges {
Styles := &Styles[n]
switch rngs.Style {
case "BOLD":
rngs.Style = "b"
case "UNDERLINE":
rngs.Style = "u"
case "ITALIC":
rngs.Style = "i"
}
Styles.From = rngs.Offset
Styles.To = rngs.Offset + rngs.Length
FT := Styles.From * Styles.To
tags[FT] = append(tags[FT], rngs.Style)
}
for n := range Styles {
Styles := &Styles[n]
Styles.TxtRaw = x.Text[Styles.From:Styles.To]
Styles.Txt = TagBuilder(Styles.TxtRaw, tags[Styles.From*Styles.To]...)
}
}
switch x.Type {
case "atomic":
if len(x.EntityRanges) != 0 {
d := entities[x.EntityRanges[0].Key]
parsedDescription.WriteString(``)
}
case "unstyled":
if l := len(Styles); l != 0 {
for n, r := range Styles {
var tag string
if x.Type == "header-two" {
tag = "h2"
}
parsedDescription.WriteString(x.Text[:r.From])
if len(urls) != 0 && len(x.EntityRanges) != 0 {
ra := &x.EntityRanges[0]
parsedDescription.WriteString(``)
parsedDescription.WriteString(r.Txt)
parsedDescription.WriteString(``)
} else if l > n+1 {
parsedDescription.WriteString(r.Txt)
}
parsedDescription.WriteString(TagBuilder(tag, x.Text[r.To:]))
}
} else {
parsedDescription.WriteString(x.Text)
}
}
parsedDescription.WriteString(" ")
}
} else if dl != 0 {
for tt := html.NewTokenizer(strings.NewReader(dscr.Html.Markup)); ; {
switch tt.Next() {
case html.ErrorToken:
return parsedDescription.String()
case html.CommentToken, html.DoctypeToken:
// No renderable content; skip.
case html.StartTagToken, html.EndTagToken, html.SelfClosingTagToken:
token := tt.Token()
switch token.Data {
case "a":
for _, a := range token.Attr {
if a.Key == "href" {
url := DeleteTrackingFromURL(a.Val)
parsedDescription.WriteString(``)
parsedDescription.WriteString(GetValueOfTag(tt))
parsedDescription.WriteString(" ")
}
}
case "img":
var uri, title string
for b, a := range token.Attr {
switch a.Key {
case "src":
if len(a.Val) > 9 && a.Val[8:9] == "e" {
uri = URLBuilder(host, "media", "emojitar", a.Val[37:len(a.Val)-4], "?type=e")
}
case "title":
title = a.Val
}
if title != "" {
for x := -1; x < b; x++ {
parsedDescription.WriteString(``)
}
}
}
case "br", "li", "ul", "p", "b":
parsedDescription.WriteString(token.String())
case "div":
parsedDescription.WriteString("