RoyalRoad: Replace regex with Parser (#1622)
* dom-serialize * prettier fix * use Parser only * prettier fix pt 2 * use URLSearchParams for popular and fix default cover * prettier trilogy (missing ;)
This commit is contained in:
@@ -3,20 +3,21 @@ import { fetchApi } from '@libs/fetch';
|
|||||||
import { Plugin } from '@typings/plugin';
|
import { Plugin } from '@typings/plugin';
|
||||||
import { Filters, FilterTypes } from '@libs/filterInputs';
|
import { Filters, FilterTypes } from '@libs/filterInputs';
|
||||||
import { NovelStatus } from '@libs/novelStatus';
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
|
import { isUrlAbsolute } from '@libs/isAbsoluteUrl';
|
||||||
|
|
||||||
class RoyalRoad implements Plugin.PluginBase {
|
class RoyalRoad implements Plugin.PluginBase {
|
||||||
id = 'royalroad';
|
id = 'royalroad';
|
||||||
name = 'Royal Road';
|
name = 'Royal Road';
|
||||||
version = '2.1.4';
|
version = '2.2.0';
|
||||||
icon = 'src/en/royalroad/icon.png';
|
icon = 'src/en/royalroad/icon.png';
|
||||||
site = 'https://www.royalroad.com/';
|
site = 'https://www.royalroad.com/';
|
||||||
|
|
||||||
parseNovels(html: string) {
|
parseNovels(html: string) {
|
||||||
const novels: Plugin.NovelItem[] = [];
|
const novels: Plugin.NovelItem[] = [];
|
||||||
let tempNovel = {} as Plugin.NovelItem;
|
let tempNovel = {} as Plugin.NovelItem;
|
||||||
|
const baseUrl = this.site;
|
||||||
tempNovel.name = '';
|
tempNovel.name = '';
|
||||||
let isParsingNovel = false;
|
let isParsingNovel = false;
|
||||||
let isNovelName = false;
|
|
||||||
const parser = new Parser({
|
const parser = new Parser({
|
||||||
onopentag(name, attribs) {
|
onopentag(name, attribs) {
|
||||||
if (attribs['class']?.includes('fiction-list-item')) {
|
if (attribs['class']?.includes('fiction-list-item')) {
|
||||||
@@ -25,10 +26,13 @@ class RoyalRoad implements Plugin.PluginBase {
|
|||||||
if (isParsingNovel) {
|
if (isParsingNovel) {
|
||||||
if (name === 'a' && attribs['class']?.includes('bold')) {
|
if (name === 'a' && attribs['class']?.includes('bold')) {
|
||||||
tempNovel.path = attribs['href'].slice(1);
|
tempNovel.path = attribs['href'].slice(1);
|
||||||
isNovelName = true;
|
|
||||||
}
|
}
|
||||||
if (name === 'img') {
|
if (name === 'img') {
|
||||||
|
tempNovel.name = attribs['alt'];
|
||||||
tempNovel.cover = attribs['src'];
|
tempNovel.cover = attribs['src'];
|
||||||
|
if (tempNovel.cover && !isUrlAbsolute(tempNovel.cover)) {
|
||||||
|
tempNovel.cover = baseUrl + tempNovel.cover.slice(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (tempNovel.path && tempNovel.name) {
|
if (tempNovel.path && tempNovel.name) {
|
||||||
novels.push(tempNovel);
|
novels.push(tempNovel);
|
||||||
@@ -37,14 +41,8 @@ class RoyalRoad implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ontext(data) {
|
|
||||||
if (isNovelName) {
|
|
||||||
tempNovel.name += data;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onclosetag(name) {
|
onclosetag(name) {
|
||||||
if (name === 'i') {
|
if (name === 'h2') {
|
||||||
isNovelName = false;
|
|
||||||
isParsingNovel = false;
|
isParsingNovel = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -61,25 +59,32 @@ class RoyalRoad implements Plugin.PluginBase {
|
|||||||
showLatestNovels,
|
showLatestNovels,
|
||||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
let link = `${this.site}fictions/search`;
|
const params = new URLSearchParams({
|
||||||
link += `?page=${page}`;
|
page: page.toString(),
|
||||||
|
});
|
||||||
|
if (showLatestNovels) {
|
||||||
|
params.append('orderBy', 'last_update');
|
||||||
|
}
|
||||||
if (!filters) filters = this.filters || {};
|
if (!filters) filters = this.filters || {};
|
||||||
if (showLatestNovels) link += '&orderBy=last_update';
|
|
||||||
for (const key in filters) {
|
for (const key in filters) {
|
||||||
if (filters[key as keyof typeof filters].value === '') continue;
|
if (filters[key as keyof typeof filters].value === '') continue;
|
||||||
if (key === 'genres' || key === 'tags' || key === 'content_warnings') {
|
if (key === 'genres' || key === 'tags' || key === 'content_warnings') {
|
||||||
if (filters[key].value.include)
|
if (filters[key].value.include) {
|
||||||
for (const include of filters[key].value.include) {
|
for (const include of filters[key].value.include) {
|
||||||
link += `&tagsAdd=${include}`;
|
params.append('tagsAdd', include);
|
||||||
}
|
}
|
||||||
if (filters[key].value.exclude)
|
}
|
||||||
|
if (filters[key].value.exclude) {
|
||||||
for (const exclude of filters[key].value.exclude) {
|
for (const exclude of filters[key].value.exclude) {
|
||||||
link += `&tagsRemove=${exclude}`;
|
params.append('tagsRemove', exclude);
|
||||||
}
|
}
|
||||||
} else if (typeof filters[key as keyof typeof filters] === 'object')
|
}
|
||||||
link += `&${key}=${filters[key as keyof typeof filters].value}`;
|
} else {
|
||||||
|
params.append(key, String(filters[key as keyof typeof filters].value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const link = `${this.site}fictions/search?${params.toString()}`;
|
||||||
const body = await fetchApi(link).then(r => r.text());
|
const body = await fetchApi(link).then(r => r.text());
|
||||||
|
|
||||||
return this.parseNovels(body);
|
return this.parseNovels(body);
|
||||||
@@ -94,6 +99,7 @@ class RoyalRoad implements Plugin.PluginBase {
|
|||||||
summary: '',
|
summary: '',
|
||||||
chapters: [],
|
chapters: [],
|
||||||
};
|
};
|
||||||
|
const baseUrl = this.site;
|
||||||
let isNovelName = false;
|
let isNovelName = false;
|
||||||
let isAuthorName = false;
|
let isAuthorName = false;
|
||||||
let isDescription = false;
|
let isDescription = false;
|
||||||
@@ -110,6 +116,9 @@ class RoyalRoad implements Plugin.PluginBase {
|
|||||||
onopentag(name, attribs) {
|
onopentag(name, attribs) {
|
||||||
if (name === 'img' && attribs['class']?.includes('thumbnail')) {
|
if (name === 'img' && attribs['class']?.includes('thumbnail')) {
|
||||||
novel.cover = attribs['src'];
|
novel.cover = attribs['src'];
|
||||||
|
if (novel.cover && !isUrlAbsolute(novel.cover)) {
|
||||||
|
novel.cover = baseUrl + novel.cover.slice(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (name === 'span' && attribs['class']?.includes('label-sm')) {
|
if (name === 'span' && attribs['class']?.includes('label-sm')) {
|
||||||
isSpan++;
|
isSpan++;
|
||||||
@@ -232,26 +241,104 @@ class RoyalRoad implements Plugin.PluginBase {
|
|||||||
async parseChapter(chapterPath: string): Promise<string> {
|
async parseChapter(chapterPath: string): Promise<string> {
|
||||||
const result = await fetchApi(this.site + chapterPath);
|
const result = await fetchApi(this.site + chapterPath);
|
||||||
const html = await result.text();
|
const html = await result.text();
|
||||||
const parts: string[] = [];
|
|
||||||
const regexPatterns: RegExp[] = [
|
|
||||||
/<style>\n\s+.(.+?){[^]+?display: none;/,
|
|
||||||
/(<div class="portlet solid author-note-portlet"[^]+?)<div class="margin-bottom-20/,
|
|
||||||
/(<div class="chapter-inner chapter-content"[^]+?)<div class="portlet light t-center-/,
|
|
||||||
/(<\/div>\s+<div class="portlet solid author-note-portlet"[^]+?)<div class="row margin-bottom-10/,
|
|
||||||
];
|
|
||||||
|
|
||||||
const extractContent = (patterns: RegExp[]) => {
|
let isChapterContent = false;
|
||||||
patterns.forEach(regex => {
|
let isAuthorNote = false;
|
||||||
const match = html.match(regex)?.[1];
|
let isBeforeNote = true;
|
||||||
if (match) parts.push(match);
|
let isHiddenContent = false;
|
||||||
});
|
let chapterHtml = '';
|
||||||
};
|
let notesHtml = '';
|
||||||
extractContent(regexPatterns);
|
let beforeNotes = '';
|
||||||
const cleanup = new RegExp(`<p class="${parts[0]}.+?</p>`, 'g');
|
let afterNotes = '';
|
||||||
const chapterText = parts.slice(1).join('<hr>');
|
let depth = 0;
|
||||||
return chapterText
|
let noteDepth = 0;
|
||||||
.replace(cleanup, '')
|
let contentDepth = 0;
|
||||||
.replace(/<p class="[^><]+>/g, '<p>');
|
let hiddenDepth = 0;
|
||||||
|
|
||||||
|
const match = html.match(/<style>\n\s+.(.+?){[^.]+?display: none;/);
|
||||||
|
const hiddenClass = match?.[1]?.trim();
|
||||||
|
|
||||||
|
const parser = new Parser({
|
||||||
|
onopentag(name, attribs) {
|
||||||
|
depth++;
|
||||||
|
if (
|
||||||
|
isHiddenContent ||
|
||||||
|
(hiddenClass && attribs['class']?.includes(hiddenClass))
|
||||||
|
) {
|
||||||
|
if (!isHiddenContent) {
|
||||||
|
isHiddenContent = true;
|
||||||
|
hiddenDepth = depth;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attribs['class']?.includes('chapter-content')) {
|
||||||
|
isChapterContent = true;
|
||||||
|
isBeforeNote = false;
|
||||||
|
contentDepth = depth;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (attribs['class']?.includes('author-note-portlet')) {
|
||||||
|
isAuthorNote = true;
|
||||||
|
noteDepth = depth;
|
||||||
|
notesHtml = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isChapterContent || isAuthorNote) {
|
||||||
|
let tag = `<${name}`;
|
||||||
|
for (const attr in attribs) {
|
||||||
|
tag += ` ${attr}="${attribs[attr].replace(/"/g, '"')}"`;
|
||||||
|
}
|
||||||
|
tag += '>';
|
||||||
|
if (isChapterContent) chapterHtml += tag;
|
||||||
|
if (isAuthorNote) notesHtml += tag;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ontext(text) {
|
||||||
|
if (isHiddenContent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isChapterContent) {
|
||||||
|
chapterHtml += text;
|
||||||
|
}
|
||||||
|
if (isAuthorNote) {
|
||||||
|
notesHtml += text;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onclosetag(name) {
|
||||||
|
if (isHiddenContent) {
|
||||||
|
if (depth === hiddenDepth) {
|
||||||
|
isHiddenContent = false;
|
||||||
|
}
|
||||||
|
depth--;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isChapterContent && depth === contentDepth) {
|
||||||
|
isChapterContent = false;
|
||||||
|
} else if (isAuthorNote && depth === noteDepth) {
|
||||||
|
isAuthorNote = false;
|
||||||
|
const fullNote = `<div class="author-note-${isBeforeNote ? 'before' : 'after'}">${notesHtml}</div>`;
|
||||||
|
if (isBeforeNote) {
|
||||||
|
beforeNotes += fullNote + '\n';
|
||||||
|
} else {
|
||||||
|
afterNotes += fullNote + '\n';
|
||||||
|
}
|
||||||
|
notesHtml = '';
|
||||||
|
} else if (isChapterContent || isAuthorNote) {
|
||||||
|
const closingTag = `</${name}>`;
|
||||||
|
if (isChapterContent) chapterHtml += closingTag;
|
||||||
|
if (isAuthorNote) notesHtml += closingTag;
|
||||||
|
}
|
||||||
|
depth--;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
parser.write(html);
|
||||||
|
parser.end();
|
||||||
|
return [beforeNotes.trim(), chapterHtml.trim(), afterNotes.trim()]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchNovels(
|
async searchNovels(
|
||||||
|
|||||||
Reference in New Issue
Block a user