From d6789522d3dd076f7c98dafa3e7d42df09252093 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Wed, 15 Jul 2026 02:25:20 -0500 Subject: fix: document exported API, fix naming, and harden Download The remaining golangci-lint findings. These land together because the Url -> URL rename spans util.go, parsers.go and wrapper.go, and splitting it would leave an intermediate commit that does not compile. Download() carried the most serious bug here: try() only prints an error, it does not return, so a failed request fell through to resp.Body.Close() on a nil resp and panicked. Every failure path now returns the zero Downloaded, and callers check Status. ReturnHTTPError guards against the resulting status 0, which would otherwise panic WriteHeader. Requests carry a context with a timeout (noctx), and a download-proxy now routes through ProxiedTransport so it keeps the DA throttle and timeouts. Also: - doc comments on all 48 exported symbols (revive's exported rule, with checkPrivateReceivers, since most of app is exported methods on the unexported skunkyart type), plus package docs in new doc.go files so both the embed and non-embed builds are covered. - ST1003 naming: UrlBuilder -> URLBuilder, id_search -> idSearch, cache_config -> cacheConfig, TXT_RAW -> TxtRaw, mediaUrl -> mediaURL. - explicit json tags on structs that are unmarshaled (musttag); the hyphenated keys already had tags, the rest relied on case-insensitive fallback. Behaviour is unchanged. - modernization: range-over-int, WaitGroup.Go, stale +build lines, interface{} -> any, strings.Builder over string concatenation in a loop. --- app/parsers.go | 110 ++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 65 insertions(+), 45 deletions(-) (limited to 'app/parsers.go') diff --git a/app/parsers.go b/app/parsers.go index 976c1b5..5f92658 100755 --- a/app/parsers.go +++ b/app/parsers.go @@ -9,6 +9,9 @@ import ( "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 :(" @@ -29,9 +32,9 @@ func (s skunkyart) ParseComments(c devianter.Comments, daError devianter.Error) cmmts.WriteString(`">

`) listContent.WriteString(data.Author.Username) listContent.WriteString(``) listContent.WriteString(data.Title) listContent.WriteString(``) listContent.WriteString(id) listContent.WriteString(``) @@ -106,7 +112,7 @@ func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, con listContent.WriteString(`

`) @@ -124,7 +130,7 @@ func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, con listContent.WriteString(`

[ TEXT ]

`) } listContent.WriteString(`
`) listContent.WriteString(data.Author.Username) listContent.WriteString(" - ") @@ -149,11 +155,12 @@ func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, con list.WriteString(``) list.WriteString(``) - if s.Type == 0 { + switch { + case s.Type == 0: list.WriteString("Daily Deviations") - } else if s.Type == 'g' && len(devs) != 0 { + case s.Type == 'g' && len(devs) != 0: list.WriteString(devs[0].Author.Username) - } else { + default: list.WriteString("SkunkyArt") } list.WriteString(``) @@ -181,19 +188,26 @@ func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, con } /* 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 - TXT_RAW string - From int - To int + Txt string + TxtRaw string + From int + To int } -// TODO: rewrite this whole mess +// 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. +// +// TODO: rewrite this whole mess. func ParseDescription(dscr devianter.Text) string { var parsedDescription strings.Builder TagBuilder := func(content string, tags ...string) string { l := len(tags) - for x := 0; x < l; x++ { + for x := range l { var htm strings.Builder htm.WriteString("<") htm.WriteString(tags[x]) @@ -208,7 +222,7 @@ func ParseDescription(dscr devianter.Text) string { } return content } - DeleteTrackingFromUrl := func(url string) string { + DeleteTrackingFromURL := func(url string) string { if len(url) > 42 && url[:42] == "https://www.deviantart.com/users/outgoing?" { url = url[42:] } @@ -220,30 +234,34 @@ func ParseDescription(dscr devianter.Text) string { description[dl-1] == '}' { var descr struct { Blocks []struct { - Text, Type string + Text string `json:"text"` + Type string `json:"type"` InlineStyleRanges []struct { - Offset, Length int - Style string - } + Offset int `json:"offset"` + Length int `json:"length"` + Style string `json:"style"` + } `json:"inlineStyleRanges"` EntityRanges []struct { - Offset, Length int - Key int - } + Offset int `json:"offset"` + Length int `json:"length"` + Key int `json:"key"` + } `json:"entityRanges"` Data struct { - TextAlignment string - } - } + TextAlignment string `json:"textAlignment"` + } `json:"data"` + } `json:"blocks"` EntityMap map[string]struct { - Type string + Type string `json:"type"` Data struct { - Url string + URL string `json:"url"` Config struct { - Aligment string - Width int - } - Data devianter.Deviation - } - } + // "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) @@ -252,8 +270,8 @@ func ParseDescription(dscr devianter.Text) string { 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) + if x.Data.URL != "" { + urls[num] = DeleteTrackingFromURL(x.Data.URL) } entities[num] = x.Data.Data } @@ -278,10 +296,10 @@ func ParseDescription(dscr devianter.Text) string { FT := Styles.From * Styles.To tags[FT] = append(tags[FT], rngs.Style) } - for n := 0; n < len(Styles); n++ { + for n := range Styles { Styles := &Styles[n] - Styles.TXT_RAW = x.Text[Styles.From:Styles.To] - Styles.TXT = TagBuilder(Styles.TXT_RAW, tags[Styles.From*Styles.To]...) + Styles.TxtRaw = x.Text[Styles.From:Styles.To] + Styles.Txt = TagBuilder(Styles.TxtRaw, tags[Styles.From*Styles.To]...) } } @@ -290,7 +308,7 @@ func ParseDescription(dscr devianter.Text) string { if len(x.EntityRanges) != 0 { d := entities[x.EntityRanges[0].Key] parsedDescription.WriteString(``) - parsedDescription.WriteString(r.TXT) + parsedDescription.WriteString(r.Txt) parsedDescription.WriteString(``) } else if l > n+1 { - parsedDescription.WriteString(r.TXT) + parsedDescription.WriteString(r.Txt) } parsedDescription.WriteString(TagBuilder(tag, x.Text[r.To:])) } @@ -332,13 +350,15 @@ func ParseDescription(dscr devianter.Text) string { 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) + url := DeleteTrackingFromURL(a.Val) parsedDescription.WriteString(``) @@ -352,7 +372,7 @@ func ParseDescription(dscr devianter.Text) string { switch a.Key { case "src": if len(a.Val) > 9 && a.Val[8:9] == "e" { - uri = UrlBuilder("media", "emojitar", a.Val[37:len(a.Val)-4], "?type=e") + uri = URLBuilder("media", "emojitar", a.Val[37:len(a.Val)-4], "?type=e") } case "title": title = a.Val -- cgit v1.2.3