mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-02 04:16:38 +08:00
b8e6418326
- 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 前端使用)
181 lines
4.5 KiB
Go
181 lines
4.5 KiB
Go
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, "[")
|
||
}
|