Files
MeBox/internal/service/reader/rule/netutil.go
T
truewhile b8e6418326 feat(reader): legado 书源规则引擎 Go 移植 + 阅读 P0 后端
- internal/service/reader/rule/:逐方法移植 legado analyzeRule 包
  (AnalyzeRule/AnalyzeByJSoup/AnalyzeByJSonPath/AnalyzeByXPath/
  AnalyzeByRegex/AnalyzeUrl/RuleAnalyzer),JS 规则留 P2 接入点
- 书源导入(JSON数组/对象/Base64/URL)、多源聚合搜索(legado 四档
  排序合并)、详情/目录/正文(nextContentUrl 翻页合并)
- 数据模型 4 表注册迁移;/api/reader/* 路由组(书源/搜索/书架/进度/调试)
- 单测 + httptest 全链路端到端测试
- docs/reader-ui-spec.md:legado UI 交互仿制规格(供 P1 前端使用)
2026-09-30 15:41:58 +08:00

181 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package rule
import (
"fmt"
"net/url"
"strings"
"golang.org/x/text/encoding/htmlindex"
"golang.org/x/text/encoding/unicode"
)
// 本文件对应 legado 的 NetworkUtils.kt / EncoderUtils.kt 中与规则引擎相关的函数。
// GetAbsoluteURL 对应 NetworkUtils.getAbsoluteURL(baseURL: String?, relativePath)。
// baseURL 会先截掉 ",{...}" 选项段(substringBefore(","))。
func GetAbsoluteURL(baseURL, relativePath string) string {
rel := strings.TrimSpace(relativePath)
if isAbsURL(rel) || isDataURL(rel) || strings.HasPrefix(rel, "javascript") {
if strings.HasPrefix(rel, "javascript") {
return ""
}
return rel
}
if baseURL == "" || isDataURL(baseURL) {
return rel
}
base := baseURL
if i := strings.Index(base, ","); i >= 0 {
base = base[:i]
}
baseURLParsed, err := url.Parse(strings.TrimSpace(base))
if err != nil {
return rel
}
return GetAbsoluteURLParsed(baseURLParsed, rel)
}
// GetAbsoluteURLParsed 对应 NetworkUtils.getAbsoluteURL(baseURL: URL?, relativePath)。
func GetAbsoluteURLParsed(base *url.URL, relativePath string) string {
rel := strings.TrimSpace(relativePath)
if base == nil {
return rel
}
if isAbsURL(rel) || isDataURL(rel) {
return rel
}
if strings.HasPrefix(rel, "javascript") {
return ""
}
ref, err := url.Parse(rel)
if err != nil {
return rel
}
return base.ResolveReference(ref).String()
}
// GetBaseUrl 对应 NetworkUtils.getBaseUrl:scheme://host[:port]。
func GetBaseUrl(u string) string {
if len(u) >= 8 && (strings.EqualFold(u[:7], "http://") || (len(u) >= 9 && strings.EqualFold(u[:8], "https://"))) {
if idx := strings.Index(u[8:], "/"); idx >= 0 {
return u[:8+idx]
}
return u
}
return ""
}
// isAbsURL 对应 String.isAbsUrl()。
func isAbsURL(s string) bool {
return strings.HasPrefix(s, "https://") || strings.HasPrefix(s, "http://")
}
func isDataURL(s string) bool {
return strings.HasPrefix(s, "data:")
}
// notNeedEncodingQuery / notNeedEncodingForm 对应 NetworkUtils 的两个 BitSet。
var (
notNeedEncodingQuery = buildEncodingSet("!$&()*+,-./:;=?@[\\]^_`{|}~")
notNeedEncodingForm = buildEncodingSet("*-._")
)
func buildEncodingSet(extra string) map[rune]bool {
set := make(map[rune]bool, 128)
for r := 'a'; r <= 'z'; r++ {
set[r] = true
}
for r := 'A'; r <= 'Z'; r++ {
set[r] = true
}
for r := '0'; r <= '9'; r++ {
set[r] = true
}
for _, r := range extra {
set[r] = true
}
return set
}
func isDigit16(r byte) bool {
return (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')
}
// encodedQuery 对应 NetworkUtils.encodedQuery(str):判断字符串是否已按
// urlEncode 规范编码(无需再编码返回 true)。
func encodedQuery(s string) bool {
return encodedWith(s, notNeedEncodingQuery)
}
// encodedForm 对应 NetworkUtils.encodedForm(str)。
func encodedForm(s string) bool {
return encodedWith(s, notNeedEncodingForm)
}
func encodedWith(s string, allow map[rune]bool) bool {
rs := []rune(s)
for i := 0; i < len(rs); i++ {
r := rs[i]
if r < 128 && allow[r] {
continue
}
if r == '%' && i+2 < len(rs) && isDigit16(byte(rs[i+1])) && isDigit16(byte(rs[i+2])) {
i += 2
continue
}
return false
}
return len(rs) > 0
}
// JSEscape 对应 EncoderUtils.escape(JS escape() 语义)。
func JSEscape(src string) string {
var sb strings.Builder
for _, r := range src {
code := int(r)
if (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122) {
sb.WriteRune(r)
continue
}
switch {
case code < 16:
fmt.Fprintf(&sb, "%%0%X", code)
case code < 256:
fmt.Fprintf(&sb, "%%%X", code)
default:
fmt.Fprintf(&sb, "%%u%04X", code)
}
}
return sb.String()
}
// DecodeBytes 按字符集名解码字节流,name 为空时按 UTF-8(带 BOM 处理)。
func DecodeBytes(b []byte, name string) (string, error) {
if len(b) >= 3 && b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF {
return string(b[3:]), nil
}
if name == "" || strings.EqualFold(name, "utf-8") || strings.EqualFold(name, "utf8") {
dec := unicode.UTF8.NewDecoder()
out, err := dec.Bytes(b)
if err != nil {
return string(b), nil
}
return string(out), nil
}
enc, err := htmlindex.Get(name)
if err != nil {
return string(b), nil
}
out, derr := enc.NewDecoder().Bytes(b)
if derr != nil {
return string(b), nil
}
return string(out), nil
}
// LooksLikeJSON 对应 String.isJson()(宽松:trim 后以 { 或 [ 开头)。
func LooksLikeJSON(s string) bool {
s = strings.TrimSpace(s)
return strings.HasPrefix(s, "{") || strings.HasPrefix(s, "[")
}