legado完善js解析
This commit is contained in:
+84
-42
@@ -60,28 +60,71 @@ function isSafeHeaderName(name: string) {
|
|||||||
return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name);
|
return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isJsRuleString(value?: string) {
|
||||||
|
const trimmed = (value || '').trim();
|
||||||
|
return /^(?:@js:|<js>)/i.test(trimmed) || /<js>[\s\S]*?<\/js>/i.test(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveLegadoDynamicValue(value: any, context: Record<string, any>, timeout = 1000): any {
|
||||||
|
if (Array.isArray(value)) return value.map((item) => resolveLegadoDynamicValue(item, context, timeout));
|
||||||
|
if (!value || typeof value !== 'object') {
|
||||||
|
if (typeof value !== 'string') return value;
|
||||||
|
if (!isJsRuleString(value)) return value;
|
||||||
|
return evaluateJsRuleString(value, context, timeout);
|
||||||
|
}
|
||||||
|
return Object.fromEntries(Object.entries(value).map(([key, val]) => [key, resolveLegadoDynamicValue(val, context, timeout)]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateJsRuleString(value: string, context: Record<string, any>, timeout = 1000) {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (/^(?:@js:|<js>)/i.test(trimmed)) return runJsSnippet(trimmed, context, timeout);
|
||||||
|
return value.replace(/<js>([\s\S]*?)<\/js>/gi, (_, code) => runJsSnippet(code, context, timeout));
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseHeaderLines(raw: string, context: Record<string, any>) {
|
||||||
|
return raw.split('\n').reduce<Record<string, string>>((headers, line) => {
|
||||||
|
const index = line.indexOf(':');
|
||||||
|
if (index <= 0) return headers;
|
||||||
|
const name = line.slice(0, index).trim();
|
||||||
|
if (!isSafeHeaderName(name)) return headers;
|
||||||
|
const value = line.slice(index + 1).trim();
|
||||||
|
headers[name] = isJsRuleString(value) ? evaluateJsRuleString(value, context) : value;
|
||||||
|
return headers;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
function asObjectHeader(value?: string | Record<string, string>, context?: Record<string, any>): Record<string, string> {
|
function asObjectHeader(value?: string | Record<string, string>, context?: Record<string, any>): Record<string, string> {
|
||||||
if (!value) return {};
|
if (!value) return {};
|
||||||
if (typeof value === 'object') return value;
|
if (typeof value === 'object') return resolveLegadoDynamicValue(value, context || {});
|
||||||
let raw = value.trim();
|
let raw = value.trim();
|
||||||
if (raw.startsWith('@js:')) {
|
if (isJsRuleString(raw)) {
|
||||||
raw = runJsSnippet(raw, context || {});
|
raw = evaluateJsRuleString(raw, context || {});
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
return parsed && typeof parsed === 'object' ? parsed : {};
|
if (!parsed || typeof parsed !== 'object') return {};
|
||||||
|
return resolveLegadoDynamicValue(parsed, context || {});
|
||||||
} catch {
|
} catch {
|
||||||
return raw.split('\n').reduce<Record<string, string>>((headers, line) => {
|
return parseHeaderLines(raw, context || {});
|
||||||
const index = line.indexOf(':');
|
|
||||||
if (index > 0) {
|
|
||||||
const name = line.slice(0, index).trim();
|
|
||||||
if (isSafeHeaderName(name)) headers[name] = line.slice(index + 1).trim();
|
|
||||||
}
|
|
||||||
return headers;
|
|
||||||
}, {});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveLegadoRule(rule?: LegadoBookSourceRule, source?: BookSource): LegadoBookSourceRule | undefined {
|
||||||
|
if (!rule) return rule;
|
||||||
|
const base = source?.url || rule.bookSourceUrl || '';
|
||||||
|
const context = { baseUrl: base, source: rule };
|
||||||
|
return resolveLegadoDynamicValue(rule, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveLegadoSource(source: BookSource): BookSource {
|
||||||
|
const rule = resolveLegadoRule(source.legado, source);
|
||||||
|
const resolved: BookSource = {
|
||||||
|
...source,
|
||||||
|
legado: rule,
|
||||||
|
};
|
||||||
|
return resolveLegadoDynamicValue(resolved, { baseUrl: source.url || rule?.bookSourceUrl || '', source: rule || source.legado || source });
|
||||||
|
}
|
||||||
|
|
||||||
function buildHeaders(source: BookSource): HeadersInit {
|
function buildHeaders(source: BookSource): HeadersInit {
|
||||||
const rule = source.legado;
|
const rule = source.legado;
|
||||||
const baseUrl = sourceBase(source);
|
const baseUrl = sourceBase(source);
|
||||||
@@ -327,11 +370,11 @@ function applyPutGetRules(rule: string, value: string) {
|
|||||||
function buildUrlFromTemplate(template: string, source: BookSource, keyword?: string, page = 1, baseOverride?: string) {
|
function buildUrlFromTemplate(template: string, source: BookSource, keyword?: string, page = 1, baseOverride?: string) {
|
||||||
const base = baseOverride || sourceBase(source);
|
const base = baseOverride || sourceBase(source);
|
||||||
let raw = template || base;
|
let raw = template || base;
|
||||||
if (/^(?:@js:|<js>)/i.test(raw.trim())) {
|
if (isJsRuleString(raw)) {
|
||||||
if (/\/k-\{\{encryptText\(key\)\}\}-\{\{page\}\}\.html/.test(raw)) {
|
if (/\/k-\{\{encryptText\(key\)\}\}-\{\{page\}\}\.html/.test(raw)) {
|
||||||
return `https://www.rrssk.com/k-${encryptTongrenKeyword(keyword || '')}-${page}.html`;
|
return `https://www.rrssk.com/k-${encryptTongrenKeyword(keyword || '')}-${page}.html`;
|
||||||
}
|
}
|
||||||
const evaluated = runJsSnippet(raw, { key: keyword || '', keyword: keyword || '', page, baseUrl: base, source: { ...(source.legado || {}), key: base } });
|
const evaluated = evaluateJsRuleString(raw, { key: keyword || '', keyword: keyword || '', page, baseUrl: base, source: { ...(source.legado || {}), key: base } });
|
||||||
raw = evaluated || raw;
|
raw = evaluated || raw;
|
||||||
if (/,(\s*)\{/.test(raw)) return raw;
|
if (/,(\s*)\{/.test(raw)) return raw;
|
||||||
}
|
}
|
||||||
@@ -460,7 +503,7 @@ function selectJsonItems(json: any, rule?: string): any[] {
|
|||||||
|
|
||||||
function evaluateJsListRule(rule: string | undefined, context: Record<string, any>): any[] {
|
function evaluateJsListRule(rule: string | undefined, context: Record<string, any>): any[] {
|
||||||
const trimmed = (rule || '').trim();
|
const trimmed = (rule || '').trim();
|
||||||
if (!/^(?:@js:|<js>)/i.test(trimmed)) return [];
|
if (!isJsRuleString(trimmed)) return [];
|
||||||
const elementRules = Array.from(trimmed.matchAll(/java\.getElements\(\s*(['"`])([\s\S]*?)\1\s*\)/g)).map((match) => match[2]).filter(Boolean);
|
const elementRules = Array.from(trimmed.matchAll(/java\.getElements\(\s*(['"`])([\s\S]*?)\1\s*\)/g)).map((match) => match[2]).filter(Boolean);
|
||||||
if (elementRules.length > 0) {
|
if (elementRules.length > 0) {
|
||||||
const raw = jsonPrimitiveToString(context.result ?? context.src ?? '');
|
const raw = jsonPrimitiveToString(context.result ?? context.src ?? '');
|
||||||
@@ -500,7 +543,7 @@ function readJsonRule(json: any, rule?: string, source?: BookSource, baseUrl?: s
|
|||||||
if (!rule) return '';
|
if (!rule) return '';
|
||||||
const trimmed = rule.trim();
|
const trimmed = rule.trim();
|
||||||
if (trimmed.includes('{{')) return renderTemplateWithJson(trimmed, json, source as BookSource, baseUrl || sourceBase(source as BookSource));
|
if (trimmed.includes('{{')) return renderTemplateWithJson(trimmed, json, source as BookSource, baseUrl || sourceBase(source as BookSource));
|
||||||
if (/^(?:@js:|<js>)/i.test(trimmed)) {
|
if (isJsRuleString(trimmed)) {
|
||||||
if (/result\s*=\s*['"]([^'"]+)['"]\s*\+\s*result\.([A-Za-z0-9_$-]+)/.test(trimmed)) {
|
if (/result\s*=\s*['"]([^'"]+)['"]\s*\+\s*result\.([A-Za-z0-9_$-]+)/.test(trimmed)) {
|
||||||
const match = trimmed.match(/result\s*=\s*['"]([^'"]+)['"]\s*\+\s*result\.([A-Za-z0-9_$-]+)/);
|
const match = trimmed.match(/result\s*=\s*['"]([^'"]+)['"]\s*\+\s*result\.([A-Za-z0-9_$-]+)/);
|
||||||
return normalizeUrl(baseUrl || sourceBase(source as BookSource), `${match?.[1] || ''}${jsonPrimitiveToString(json?.[match?.[2] || ''])}`);
|
return normalizeUrl(baseUrl || sourceBase(source as BookSource), `${match?.[1] || ''}${jsonPrimitiveToString(json?.[match?.[2] || ''])}`);
|
||||||
@@ -779,13 +822,8 @@ function readValue($: cheerio.CheerioAPI, root: cheerio.Cheerio<any>, rule?: str
|
|||||||
if (value) return value;
|
if (value) return value;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (/^<js>/i.test(alternative.trim())) {
|
if (isJsRuleString(alternative)) {
|
||||||
const transformed = runJsSnippet(alternative, { ...(jsContext || {}), result: $.html(root), src: $.html(root), baseUrl });
|
const transformed = evaluateJsRuleString(alternative, { ...(jsContext || {}), result: $.html(root), src: $.html(root), baseUrl });
|
||||||
if (transformed) return /^(?:https?:)?\/\//i.test(transformed) || transformed.startsWith('/') ? normalizeUrl(baseUrl || '', transformed) : transformed;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (/^@js:/i.test(alternative.trim())) {
|
|
||||||
const transformed = runJsSnippet(alternative, { ...(jsContext || {}), result: $.html(root), src: $.html(root), baseUrl });
|
|
||||||
if (transformed) return /^(?:https?:)?\/\//i.test(transformed) || transformed.startsWith('/') ? normalizeUrl(baseUrl || '', transformed) : transformed;
|
if (transformed) return /^(?:https?:)?\/\//i.test(transformed) || transformed.startsWith('/') ? normalizeUrl(baseUrl || '', transformed) : transformed;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -955,7 +993,7 @@ function applyContentJsRule(value: string, jsRule: string): string {
|
|||||||
|
|
||||||
function contentFromRule(raw: string, rule?: string, baseUrl?: string): string {
|
function contentFromRule(raw: string, rule?: string, baseUrl?: string): string {
|
||||||
const json = parseJsonMaybe(raw);
|
const json = parseJsonMaybe(raw);
|
||||||
if (json && (ruleIsJson(rule) || rule?.trim().startsWith('@js:'))) {
|
if (json && (ruleIsJson(rule) || isJsRuleString(rule))) {
|
||||||
return readJsonRule(json, rule, undefined, baseUrl);
|
return readJsonRule(json, rule, undefined, baseUrl);
|
||||||
}
|
}
|
||||||
const rawRule = rule || '';
|
const rawRule = rule || '';
|
||||||
@@ -1056,7 +1094,8 @@ export function normalizeImportedSources(input: unknown): BookSource[] {
|
|||||||
legado: rule,
|
legado: rule,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((source) => !!source.url);
|
.filter((source) => !!source.url)
|
||||||
|
.map((source) => resolveLegadoSource(source));
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeConfiguredLegadoSource(item: any, index: number): BookSource | null {
|
function normalizeConfiguredLegadoSource(item: any, index: number): BookSource | null {
|
||||||
@@ -1066,7 +1105,7 @@ function normalizeConfiguredLegadoSource(item: any, index: number): BookSource |
|
|||||||
const name = item.name || rule.bookSourceName || `Legado 书源 ${index + 1}`;
|
const name = item.name || rule.bookSourceName || `Legado 书源 ${index + 1}`;
|
||||||
const url = item.url || rule.bookSourceUrl || '';
|
const url = item.url || rule.bookSourceUrl || '';
|
||||||
if (!url) return null;
|
if (!url) return null;
|
||||||
return {
|
return resolveLegadoSource({
|
||||||
...item,
|
...item,
|
||||||
id: item.id || `legado_${stableId(`${name}|${url}|${index}`)}`,
|
id: item.id || `legado_${stableId(`${name}|${url}|${index}`)}`,
|
||||||
name,
|
name,
|
||||||
@@ -1075,7 +1114,7 @@ function normalizeConfiguredLegadoSource(item: any, index: number): BookSource |
|
|||||||
enabled: item.enabled !== false && rule.enabled !== false,
|
enabled: item.enabled !== false && rule.enabled !== false,
|
||||||
authMode: item.authMode || 'none',
|
authMode: item.authMode || 'none',
|
||||||
legado: { ...rule, bookSourceName: rule.bookSourceName || name, bookSourceUrl: rule.bookSourceUrl || url },
|
legado: { ...rule, bookSourceName: rule.bookSourceName || name, bookSourceUrl: rule.bookSourceUrl || url },
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
if (item.bookSourceUrl || item.searchUrl || item.ruleSearch) {
|
if (item.bookSourceUrl || item.searchUrl || item.ruleSearch) {
|
||||||
return normalizeImportedSources([item])[0] || null;
|
return normalizeImportedSources([item])[0] || null;
|
||||||
@@ -1205,7 +1244,7 @@ async function getSourceById(sourceId: string): Promise<BookSource> {
|
|||||||
|
|
||||||
function getRule(source: BookSource): LegadoBookSourceRule {
|
function getRule(source: BookSource): LegadoBookSourceRule {
|
||||||
if (!source.legado) throw new Error('Legado 书源缺少规则');
|
if (!source.legado) throw new Error('Legado 书源缺少规则');
|
||||||
return source.legado;
|
return resolveLegadoRule(source.legado, source) || source.legado;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeItem(source: BookSource, partial: Partial<BookListItem> & { detailHref?: string; title?: string }): BookListItem {
|
function makeItem(source: BookSource, partial: Partial<BookListItem> & { detailHref?: string; title?: string }): BookListItem {
|
||||||
@@ -1241,13 +1280,13 @@ function getEffectiveExploreRule(rule: LegadoBookSourceRule): LegadoRuleSearch |
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hasExplore(rule: LegadoBookSourceRule) {
|
function hasExplore(rule: LegadoBookSourceRule) {
|
||||||
return rule.enabledExplore !== false && (!!rule.exploreUrl?.trim().startsWith('@js:') || parseExploreUrl(rule.exploreUrl).length > 0) && !!getEffectiveExploreRule(rule);
|
return rule.enabledExplore !== false && (isJsRuleString(rule.exploreUrl) || parseExploreUrl(rule.exploreUrl).length > 0) && !!getEffectiveExploreRule(rule);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseExploreUrl(exploreUrl?: string): Array<{ title: string; template: string }> {
|
function parseExploreUrl(exploreUrl?: string): Array<{ title: string; template: string }> {
|
||||||
const raw = (exploreUrl || '').trim();
|
const raw = (exploreUrl || '').trim();
|
||||||
if (!raw) return [];
|
if (!raw) return [];
|
||||||
if (raw.startsWith('@js:')) return [];
|
if (isJsRuleString(raw)) return [];
|
||||||
const json = parseJsonMaybe(raw);
|
const json = parseJsonMaybe(raw);
|
||||||
if (Array.isArray(json)) {
|
if (Array.isArray(json)) {
|
||||||
return json
|
return json
|
||||||
@@ -1288,7 +1327,7 @@ function buildExploreTargetUrl(source: BookSource, target: ExploreTarget) {
|
|||||||
|
|
||||||
async function resolveExploreCategories(source: BookSource, rule: LegadoBookSourceRule): Promise<Array<{ title: string; template: string }>> {
|
async function resolveExploreCategories(source: BookSource, rule: LegadoBookSourceRule): Promise<Array<{ title: string; template: string }>> {
|
||||||
const raw = (rule.exploreUrl || '').trim();
|
const raw = (rule.exploreUrl || '').trim();
|
||||||
if (!raw.startsWith('@js:')) return parseExploreUrl(raw);
|
if (!isJsRuleString(raw)) return parseExploreUrl(raw);
|
||||||
|
|
||||||
const jsValue = runJsSnippetRaw(raw, { baseUrl: sourceBase(source), source: rule });
|
const jsValue = runJsSnippetRaw(raw, { baseUrl: sourceBase(source), source: rule });
|
||||||
const parsed = Array.isArray(jsValue) ? jsValue : typeof jsValue === 'string' ? parseJsonMaybe(jsValue) : null;
|
const parsed = Array.isArray(jsValue) ? jsValue : typeof jsValue === 'string' ? parseJsonMaybe(jsValue) : null;
|
||||||
@@ -1318,17 +1357,20 @@ export class LegadoClient {
|
|||||||
async getSources(): Promise<BookSource[]> {
|
async getSources(): Promise<BookSource[]> {
|
||||||
const config = await resolveLegadoConfig();
|
const config = await resolveLegadoConfig();
|
||||||
if (!config.enabled) return [];
|
if (!config.enabled) return [];
|
||||||
return config.sources.map((source) => ({
|
return config.sources.map((source) => {
|
||||||
...source,
|
const resolved = resolveLegadoSource(source);
|
||||||
capabilities: {
|
return {
|
||||||
searchSupported: !!source.legado?.searchUrl,
|
...resolved,
|
||||||
catalogSupported: hasExplore(source.legado || {}),
|
capabilities: {
|
||||||
searchMode: source.legado?.searchUrl ? 'legado' : 'disabled',
|
searchSupported: !!resolved.legado?.searchUrl,
|
||||||
catalogMode: hasExplore(source.legado || {}) ? 'legado' : 'disabled',
|
catalogSupported: hasExplore(resolved.legado || {}),
|
||||||
acquisitionTypes: ['application/x-legado-chapters+json'],
|
searchMode: resolved.legado?.searchUrl ? 'legado' : 'disabled',
|
||||||
lastCheckedAt: Date.now(),
|
catalogMode: hasExplore(resolved.legado || {}) ? 'legado' : 'disabled',
|
||||||
},
|
acquisitionTypes: ['application/x-legado-chapters+json'],
|
||||||
}));
|
lastCheckedAt: Date.now(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSearchSources(sourceId?: string): Promise<BookSource[]> {
|
async getSearchSources(sourceId?: string): Promise<BookSource[]> {
|
||||||
|
|||||||
Reference in New Issue
Block a user