mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-07 05:46:38 +08:00
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 前端使用)
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
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, "[")
|
||||
}
|
||||
Reference in New Issue
Block a user